4.2.3. Использование std::promise

При написании сетевых серверных программ часто возникает искушение обрабатывать каждый запрос на соединение в отдельном потоке, поскольку при такой структуре порядок коммуникации становится нагляднее и проще для программирования. Этот подход срабатывает, пока количество соединений (и, следовательно, потоков) не слишком велико. Но с ростом числа потоков увеличивается и объем потребляемых ресурсов операционной системы, а равно частота контекстных переключений (если число потоков превышает уровень аппаратного параллелизма), что негативно сказывается на производительности. В предельном случае у операционной системы могут закончиться ресурсы для запуска новых потоков, хотя пропускная способность сети еще не исчерпана. Поэтому в приложениях, обслуживающих очень большое число соединений, обычно создают совсем немного потоков (быть может, всего один), каждый из которых одновременно обрабатывает несколько запросов.

Рассмотрим один из таких потоков. Пакеты данных приходят по разным соединениям в случайном порядке, а потому и порядок помещения исходящих пакетов в очередь отправки тоже непредсказуем. Часто будет складываться ситуация, когда другие части приложения ждут либо успешной отправки данных, либо поступления нового пакета по конкретному сетевому соединению.

Шаблон std::promise<T> дает возможность задать значение (типа T), которое впоследствии можно будет прочитать с помощью ассоциированного объекта std::future<T>. Пара std::promise/std::future реализует один из возможных механизмов такого рода; ожидающий поток приостанавливается в ожидании будущего результата, тогда как поток, поставляющий данные, может с помощью promise установить ассоциированное значение и сделать будущий результат готовым.

Чтобы получить объект std::future, ассоциированный с данным обещанием std::promise, мы должны вызвать функцию-член get_future() — так же, как в случае std::packaged_task. После установки значения обещания (с помощью функции-члена set_value()) будущий результат становится готовым, и его можно использовать для получения установленного значения. Если уничтожить объект std::promise, не установив значение, то в будущем результате будет сохранено исключение. О передаче исключений между потоками см. раздел 4.2.4.

В листинге 4.10 приведен код потока обработки соединений, написанный по только что изложенной схеме. В данном случае для уведомления об успешной передаче блока исходящих данных применяется пара std::promise<bool>/std::future<bool>; ассоциированное с будущим результатом значение — это просто булевский флаг успех/неудача. Для входящих пакетов в качестве ассоциированных данных могла бы выступать полезная нагрузка пакета.

Листинг 4.10. Обработка нескольких соединений в одном потоке с помощью объектов-обещаний

#include <future>

void process_connections(connection_set& connections) {

 while(!done(connections)) {             ← (1)

  for (connection_iterator               ← (2)

   connection = connections.begin(), end = connections.end();

   connection != end;

   ++connection) {

   if (connection->has_incoming_data()) {← (3)

    data_packet data = connection->incoming();

    std::promise<payload_type>& p =

     connection->get_promise(data.id);   ← (4)

    p.set_value(data.payload);

   }

   if (connection->has_outgoing_data()) {← (5)

    outgoing_packet data =

     connection->top_of_outgoing_queue();

    connection->send(data.payload);

    data.promise.set_value(true);        ← (6)

   }

  }

 }

}

Функция process_connections() повторяет цикл, пока done() возвращает true (1). На каждой итерации поочередно проверяется каждое соединение (2); если есть входящие данные, они читаются (3), а если в очереди имеются исходящие данные, они отсылаются (5). При этом предполагается, что в каждом входящем пакете хранится некоторый идентификатор и полезная нагрузка, содержащая собственно данные. Идентификатору сопоставляется объект std::promise (возможно, путем поиска в ассоциативном контейнере) (4), значением которого является полезная нагрузка пакета. Исходящие пакеты просто извлекаются из очереди отправки и передаются но соединению. После завершения передачи в обещание, ассоциированное с исходящими данными, записывается значение true, обозначающее успех (6). Насколько хорошо эта схема ложится на фактический сетевой протокол, зависит от самого протокола; в конкретном случае схема обещание/будущий результат может и не подойти, хотя структурно она аналогична поддержке асинхронного ввода/вывода в некоторых операционных системах.

В коде выше мы полностью проигнорировали возможные исключения. Хотя мир, в котором всё всегда работает правильно, был бы прекрасен, действительность не так радужна. Переполняются диски, не находятся искомые данные, отказывает сеть, «падает» база данных — всякое бывает. Если бы операция выполнялась в том потоке, которому нужен результат, программа могла бы просто сообщить об ошибке с помощью исключения. Но было бы неоправданным ограничением требовать, чтобы всё работало правильно только потому, что мы захотели воспользоваться классами std::packaged_task или std::promise.

Поэтому в стандартной библиотеке С++ имеется корректный способ учесть возникновение исключений в таком контексте и сохранить их как часть ассоциированного результата.