Почему std::async медленный по сравнению с простыми отдельными потоками?

Мне несколько раз говорили, что я должен использовать std::async для огня и забудьте тип задач с std::launch::async параметр (так что он делает это волшебство в новом потоке выполнения предпочтительно).

Воодушевленный этими заявлениями, я хотел посмотреть, как std::async по сравнению с:

  • последовательное выполнение
  • простой отдельный std::thread
  • моя простая асинхронная "реализация"

Моя наивная асинхронная реализация выглядит так:

template <typename F, typename... Args>
auto myAsync(F&& f, Args&&... args) -> std::future<decltype(f(args...))>
{
    std::packaged_task<decltype(f(args...))()> task(std::bind(std::forward<F>(f), std::forward<Args>(args)...));
    auto future = task.get_future();

    std::thread thread(std::move(task));
    thread.detach();

    return future;
}

Ничего особенного, упаковывает функтор f в std::packaged task вместе с его аргументами запускает его по новой std::thread который отделен, и возвращается с std::future из задачи.

А теперь код, измеряющий время выполнения с std::chrono::high_resolution_clock:

int main(void)
{
    constexpr unsigned short TIMES = 1000;

    auto start = std::chrono::high_resolution_clock::now();
    for (int i = 0; i < TIMES; ++i)
    {
        someTask();
    }
    auto dur = std::chrono::high_resolution_clock::now() - start;

    auto tstart = std::chrono::high_resolution_clock::now();
    for (int i = 0; i < TIMES; ++i)
    {
        std::thread t(someTask);
        t.detach();
    }
    auto tdur = std::chrono::high_resolution_clock::now() - tstart;

    std::future<void> f;
    auto astart = std::chrono::high_resolution_clock::now();
    for (int i = 0; i < TIMES; ++i)
    {
        f = std::async(std::launch::async, someTask);
    }
    auto adur = std::chrono::high_resolution_clock::now() - astart;

    auto mastart = std::chrono::high_resolution_clock::now();
    for (int i = 0; i < TIMES; ++i)
    {
        f = myAsync(someTask);
    }
    auto madur = std::chrono::high_resolution_clock::now() - mastart;

    std::cout << "Simple: " << std::chrono::duration_cast<std::chrono::microseconds>(dur).count() <<
    std::endl << "Threaded: " << std::chrono::duration_cast<std::chrono::microseconds>(tdur).count() <<
    std::endl << "std::sync: " << std::chrono::duration_cast<std::chrono::microseconds>(adur).count() <<
    std::endl << "My async: " << std::chrono::duration_cast<std::chrono::microseconds>(madur).count() << std::endl;

    return EXIT_SUCCESS;
}

куда someTask() это простой метод, где я немного подожду, имитируя проделанную работу:

void someTask()
{
    std::this_thread::sleep_for(std::chrono::milliseconds(1));
}

Наконец, мои результаты:

  • Последовательный: 1263615
  • С резьбой: 47111
  • std:: sync: 821441
  • Мой асинхронный: 30784

Может ли кто-нибудь объяснить эти результаты? Это похоже на std::aysnc намного медленнее, чем моя наивная реализация, или просто и просто std::thread s. Почему это? После этих результатов есть ли основания использовать std::async?

(Обратите внимание, что я сделал этот тест с clang++ и g ++, и результаты были очень похожи)

ОБНОВИТЬ:

После прочтения ответа Дэйва С. я обновил свой небольшой тест следующим образом:

std::future<void> f[TIMES];
auto astart = std::chrono::high_resolution_clock::now();
for (int i = 0; i < TIMES; ++i)
{
    f[i] = std::async(std::launch::async, someTask);
}
auto adur = std::chrono::high_resolution_clock::now() - astart;

Итак std::future Теперь они не уничтожены - и, таким образом, присоединены - каждый запуск. После этого изменения в коде, std::async дает результаты, аналогичные моей реализации и отделен std::thread s.

2 ответа

Решение

Одно ключевое отличие состоит в том, что будущее, возвращаемое асинхронным соединением, присоединяется к потоку, когда будущее уничтожается или, в вашем случае, заменяется новым значением.

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

sts::async возвращает специальный std::future, Это будущее имеет ~future это делает .wait(),

Так что ваши примеры принципиально разные. Медленные на самом деле делают задачи во время вашего времени. Быстрые просто ставят в очередь задачи и забывают, как узнать, что задача выполнена. Поскольку поведение программ, позволяющих потокам проходить после окончания main, непредсказуемо, его следует избегать.

Правильный способ сравнить задачи - сохранить полученные результаты. future при генерации и до окончания таймера .wait()/.join() их всех, или избегайте уничтожения объектов, пока не истечет таймер. Этот последний случай, однако, заставляет версию seuential выглядеть хуже, чем она есть.

Вам нужно присоединиться / подождать перед началом следующего теста, так как в противном случае вы крадете ресурсы из их времени.

Обратите внимание, что перемещенные фьючерсы удаляют ожидание из источника.

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