В чем разница между 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, который в конечном итоге будет содержать результат вызова этой функции".

Другие вопросы по тегам