В чем разница между packaged_task и async
Работая с многопоточной моделью C++11, я заметил, что
std::packaged_task<int(int,int)> task([](int a, int b) { return a + b; });
auto f = task.get_future();
task(2,3);
std::cout << f.get() << '\n';
а также
auto f = std::async(std::launch::async,
[](int a, int b) { return a + b; }, 2, 3);
std::cout << f.get() << '\n';
кажется, делают то же самое. Я понимаю, что может быть большая разница, если я побежал std::async
с std::launch::deferred
, но есть ли в этом случае?
В чем разница между этими двумя подходами и, что более важно, в каких случаях мне следует использовать один над другим?
4 ответа
На самом деле, приведенный вами пример показывает различия, если вы используете довольно длинную функцию, такую как
//! sleeps for one second and returns 1
auto sleep = [](){
std::this_thread::sleep_for(std::chrono::seconds(1));
return 1;
};
Пакетное задание
packaged_task
не начнется сам по себе, вы должны вызвать его:
std::packaged_task<int()> task(sleep);
auto f = task.get_future();
task(); // invoke the function
// You have to wait until task returns. Since task calls sleep
// you will have to wait at least 1 second.
std::cout << "You can see this after 1 second\n";
// However, f.get() will be available, since task has already finished.
std::cout << f.get() << std::endl;
std::async
С другой стороны, std::async
с launch::async
попытается запустить задачу в другом потоке:
auto f = std::async(std::launch::async, sleep);
std::cout << "You can see this immediately!\n";
// However, the value of the future will be available after sleep has finished
// so f.get() can block up to 1 second.
std::cout << f.get() << "This will be shown after a second!\n";
недостаток
Но прежде чем пытаться использовать async
для всего, имейте в виду, что возвращенное будущее имеет особое общее состояние, которое требует, чтобы future::~future
блоки:
std::async(do_work1); // ~future blocks
std::async(do_work2); // ~future blocks
/* output: (assuming that do_work* log their progress)
do_work1() started;
do_work1() stopped;
do_work2() started;
do_work2() stopped;
*/
Так что если вы хотите по-настоящему асинхронный, вам нужно сохранить future
или если вас не волнует результат, если обстоятельства изменятся:
{
auto pizza = std::async(get_pizza);
/* ... */
if(need_to_go)
return; // ~future will block
else
eat(pizza.get());
}
Для получения дополнительной информации об этом, см. Статью Херба Саттера async
а также ~future
, который описывает проблему, и Скотт Мейер std::futures
от std::async
не особенные, которые описывают идеи. Также обратите внимание, что это поведение было указано в C++14 и выше, но также обычно реализовано в C++11.
Дальнейшие различия
Используя std::async
Вы больше не можете выполнять свою задачу в определенном потоке, где std::packaged_task
можно перенести в другие темы.
std::packaged_task<int(int,int)> task(...);
auto f = task.get_future();
std::thread myThread(std::move(task),2,3);
std::cout << f.get() << "\n";
Также packaged_task
должен быть вызван, прежде чем позвонить f.get()
иначе ваша программа замерзнет, так как будущее никогда не станет готовым:
std::packaged_task<int(int,int)> task(...);
auto f = task.get_future();
std::cout << f.get() << "\n"; // oops!
task(2,3);
TL; DR
использование std::async
если вы хотите, чтобы что-то было сделано, и вам все равно, когда они будут сделаны, и std::packaged_task
если вы хотите обернуть вещи, чтобы переместить их в другие потоки или вызвать их позже. Или, цитируя Кристиана:
В конце концов
std::packaged_task
это просто функция более низкого уровня для реализацииstd::async
(именно поэтому он может сделать больше, чемstd::async
если используется вместе с другими вещами более низкого уровня, такими какstd::thread
). Просто произнесstd::packaged_task
этоstd::function
связано сstd::future
а такжеstd::async
оборачивает и вызываетstd::packaged_task
(возможно, в другой теме).
TL;DR
позволяет нам получить «ограниченный» какой-либо вызываемый объект, а затем контролировать, когда и где этот вызываемый объект будет выполняться без необходимости в этом будущем объекте.
позволяет первое, но не второе. А именно, он позволяет нам получить будущее для некоторого вызываемого объекта, но тогда мы не можем контролировать его выполнение без этого будущего объекта.
Практический пример
Вот практический пример проблемы, которую можно решить с помощью, но не с помощью .
Предположим, вы хотите реализовать пул потоков . Он состоит из фиксированного количества рабочих потоков и общей очереди . Но общая очередь чего?
std::packaged_task
здесь вполне подходит.
template <typename T>
class ThreadPool {
public:
using task_type = std::packaged_task<T()>;
std::future<T> enqueue(task_type task) {
// could be passed by reference as well...
// ...or implemented with perfect forwarding
std::future<T> res = task.get_future();
{ std::lock_guard<std::mutex> lock(mutex_);
tasks_.push(std::move(task));
}
cv_.notify_one();
return res;
}
void worker() {
while (true) { // supposed to be run forever for simplicity
task_type task;
{ std::unique_lock<std::mutex> lock(mutex_);
cv_.wait(lock, [this]{ return !this->tasks_.empty(); });
task = std::move(tasks_.top());
tasks_.pop();
}
task();
}
}
... // constructors, destructor,...
private:
std::vector<std::thread> workers_;
std::queue<task_type> tasks_;
std::mutex mutex_;
std::condition_variable cv_;
};
Такая функциональность не может быть реализована с помощью . Нам нужно вернуть из
enqueue()
. Если бы мы позвонили
std::async
туда (даже с отложенной политикой) и вернуться
std::future
, то у нас не было бы выбора, как выполнить вызываемый объект в
worker()
. Обратите внимание, что вы не можете создать несколько фьючерсов для одного и того же общего состояния (фьючерсы нельзя копировать).
Пакетная задача против асинхронного
p> Упакованная задача содержит задачу [function or function object]
и пара будущее / обещание. Когда задача выполняет инструкцию возврата, она вызывает set_value(..)
на packaged_task
обещание
a> Учитывая будущее, обещание и пакетную задачу, мы можем создавать простые задачи, не слишком заботясь о потоках [поток - это то, что мы даем для запуска задачи].
Однако нам нужно учитывать, сколько потоков использовать, или лучше ли выполнить задачу в текущем потоке или в другом и т. Д. Такие решения могут обрабатываться средством запуска потоков, называемым async()
, который решает, создавать ли новый поток или перезапускать старый, или просто запускать задачу в текущем потоке. Это возвращает будущее.
"Шаблон класса std::packaged_task оборачивает любую вызываемую цель (функцию, лямбда-выражение, выражение привязки или другой объект функции), чтобы его можно было вызывать асинхронно. Его возвращаемое значение или выброшенное исключение хранятся в общем состоянии, к которому можно получить доступ через std:: будущие объекты. "
"Функция шаблона async запускает функцию f асинхронно (возможно, в отдельном потоке) и возвращает std:: future, который в конечном итоге будет содержать результат вызова этой функции".