Выбрать главу

bool gui_shutdown_message_received();

void get_and_process_gui_message();

void gui_thread() {                         ← (1)

 while (!gui_shutdown_message_received()) { ← (2)

  get_and_process_gui_message();            ← (3)

  std::packaged_task<void()> task; {

   std::lock_guard<std::mutex> lk(m);

   if (tasks empty())                       ← (4)

    continue;

   task = std::move(tasks.front());         ← (5)

   tasks.pop_front();

  }

 task();                                    ← (6)

 }

}

std::thread gui_bg_thread(gui_thread);

template<typename Func>

std::future<void> post_task_for_gui_thread(Func f) {

 std::packaged_task<void()> task(f);       ← (7)

 std::future<void> res = task.get_future();← (8)

 std::lock_guard<std::mutex> lk(m);

 tasks.push_back(std::move(task));         ← (9)

 return res;                               ← (10)

}

Код очень простой: поток пользовательского интерфейса (1) повторяет цикл, пока не будет получено сообщение о необходимости завершить работу (2). На каждой итерации проверяется, есть ли готовые для обработки сообщения GUI (3), например события мыши, или новые задачи в очереди. Если задач нет (4), программа переходит на начало цикла; в противном случае извлекает задачу из очереди (5), освобождает защищающий очередь мьютекс и исполняет задачу (6). По завершении задачи будет готов ассоциированный с ней будущий результат.

Помещение задачи в очередь ничуть не сложнее: по предоставленной функции создается новая упакованная задача (7), для получения ее будущего результата вызывается функция-член get_future() (8), после чего задача помещается в очередь (9) еще до того, как станет доступен будущий результат (10). Затем часть программы, которая отправляла сообщение потоку пользовательского интерфейса, может дождаться будущего результата, если хочет знать, как завершилась задача, или отбросить его, если это несущественно.

В этом примере мы использовали класс std::packaged_task<void()> для задач, обертывающих функцию или иной допускающий вызов объект, который не принимает параметров и возвращает void (если он вернет что-то другое, то возвращенное значение будет отброшено). Это простейшая из всех возможных задач, но, как мы видели ранее, шаблон std::packaged_task применим и в более сложных ситуациях — задав другую сигнатуру функции в качестве параметра шаблона, вы сможете изменить тип возвращаемого значения (и, стало быть, тип данных, которые хранятся в состоянии, ассоциированном с будущим объектом), а также типы аргументов оператора вызова. Наш пример легко обобщается на задачи, которые должны выполняться в потоке GUI и при этом принимают аргументы и возвращают в std::future какие-то данные, а не только индикатор успешности завершения.

А как быть с задачами, которые нельзя выразить в виде простого вызова функции, или такими, где результат может поступать из нескольких мест? Эти проблемы решаются с помощью еще одного способа создания будущего результата: явного задания значения с помощью шаблона класса std::promise[8].

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

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

вернуться

8

promise — обещание. Прим. перев.