4.2.2. Ассоциирование задачи с будущим результатом

Шаблон класса std::packaged_task<> связывает будущий результат с функцией или объектом, допускающим вызов. При вызове объекта std::packaged_task<> ассоциированная функция или допускающий вызов объект вызывается и делает будущий результат готовым, сохраняя возвращенное значение в виде ассоциированных данных. Этот механизм можно использовать для построение пулов потоков (см. главу 9) и иных схем управления, например, запускать каждую задачу в отдельном потоке или запускать их все последовательно в выделенном фоновом потоке. Если длительную операцию можно разбить на автономные подзадачи, то каждую из них можно обернуть объектом std::packaged_task<> и передать этот объект планировщику задач или пулу потоков. Таким образом, мы абстрагируем специфику задачи — планировщик имеет дело только с экземплярами std::packaged_task<>, а не с индивидуальными функциями.

Параметром шаблона класса std::packaged_task<> является сигнатура функции, например void() для функции, которая не принимает никаких параметров и не возвращает значения, или int(std::string&, double*) для функции, которая принимает неконстантную ссылку на std::string и указатель на double и возвращает значение типа int. При конструировании экземпляра std::packaged_task вы обязаны передать функцию или допускающий вызов объект, который принимает параметры указанных типов и возвращает значение типа, преобразуемого в указанный тип возвращаемого значения. Точного совпадения типов не требуется; можно сконструировать объект std::packaged_task<double (double)> из функции, которая принимает int и возвращает float, потому что между этими типами существуют неявные преобразования.

Тип возвращаемого значения, указанный в сигнатуре функции, определяет тип объекта std::future<>, возвращаемого функцией-членом get_future(), а заданный в сигнатуре список аргументов используется для определения сигнатуры оператора вызова в классе упакованной задачи. Например, в листинге ниже приведена часть определения класса std::packaged_task<std::string(std::vector<char>*, int)>.

Листинг 4.8. Определение частичной специализации std::packaged_task

template<>

class packaged_task<std::string(std::vector<char>*, int)> {

public:

 template<typename Callable>

 explicit packaged_task(Callable&& f);

 std::future<std::string> get_future();

 void operator()(std::vector<char>*, int);

};

Таким образом, std::packaged_task — допускающий вызов объект, и, значит, его можно обернуть объектом std::function, передать std::thread в качестве функции потока, передать любой другой функции, которая ожидает допускающий вызов объект, или даже вызвать напрямую. Если std::packaged_task вызывается как объект-функция, то аргументы, переданные оператору вызова, без изменения передаются обернутой им функции, а возвращенное значение сохраняется в виде асинхронного результата в объекте std::future, полученном от get_future(). Следовательно, мы можем обернуть задачу в std::packaged_task и извлечь будущий результат перед тем, как передавать объект std::packaged_task в то место, из которого он будет в свое время вызван. Когда результат понадобится, нужно будет подождать готовности будущего результата. В следующем примере показано, как всё это делается на практике.

Передача задач между потоками

Во многих каркасах для разработки пользовательского интерфейса требуется, чтобы интерфейс обновлялся только в специально выделенных потоках. Если какому-то другому потоку потребуется обновить интерфейс, то он должен послать сообщение одному из таких выделенных потоков, чтобы тот выполнил операцию. Шаблон std::packaged_task позволяет решить эту задачу, не заводя специальных сообщений для каждой относящейся к пользовательскому интерфейсу операции.

Листинг 4.9. Выполнение кода в потоке пользовательского интерфейса с применением std::packaged_task

#include <deque>

#include <mutex>

#include <future>

#include <thread>

#include <utility>

std::mutex m;

std::deque<std::packaged_task<void()>> tasks;

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].