Почему 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 выглядеть хуже, чем она есть.
Вам нужно присоединиться / подождать перед началом следующего теста, так как в противном случае вы крадете ресурсы из их времени.
Обратите внимание, что перемещенные фьючерсы удаляют ожидание из источника.