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;