Делает ли async (launch :: async) в С++ 11 пулами потоков устаревшими для избежания дорогостоящего создания потоков?

Это слабо связано с этим вопросом: объединены ли std :: thread в С++ 11? , Хотя вопрос отличается, намерение то же самое:

Вопрос 1. Имеет ли смысл использовать собственные (или сторонние библиотеки) пулы потоков, чтобы избежать дорогостоящего создания потоков?

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

Не думаю, что это навязывается стандартом, но IMHO, я ожидаю, что все хорошие реализации С++ 11 будут использовать пул потоков, если создание потоков будет медленным. Я предполагаю, что только на платформах, где создание нового потока недорого, они всегда создают новый поток.

Вопрос 2: Это именно то, что я думаю, но у меня нет фактов, чтобы это доказать. Я вполне могу ошибаться. Это обоснованное предположение?

Наконец, здесь я предоставил некоторый пример кода, который сначала показывает, как я думаю, что создание потока может быть выражено async(launch::async):

Пример 1:

 thread t([]{ f(); });
 // ...
 t.join();

становится

 auto future = async(launch::async, []{ f(); });
 // ...
 future.wait();

Пример 2: запустить и забыть нить

 thread([]{ f(); }).detach();

становится

 // a bit clumsy...
 auto dummy = async(launch::async, []{ f(); });

 // ... but I hope soon it can be simplified to
 async(launch::async, []{ f(); });

Вопрос 3: Вы бы предпочли async версии версиям thread?


Остальное больше не является частью вопроса, но только для уточнения:

Почему возвращаемое значение должно быть присвоено фиктивной переменной?

К сожалению, текущий стандарт С++ 11 требует, чтобы вы захватили возвращаемое значение std::async, так как в противном случае выполняется деструктор, который блокируется до завершения действия. Это считается ошибкой в стандарте (например, Хербом Саттером).

Этот пример cppreference.com прекрасно иллюстрирует это:

{
  std::async(std::launch::async, []{ f(); });
  std::async(std::launch::async, []{ g(); });  // does not run until f() completes
}

Еще одно уточнение:

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

Я думаю, что все еще существуют ситуации, когда пулы потоков очень полезны, особенно если вам нужен больший контроль над ресурсами. Например, сервер может решить обрабатывать только фиксированное количество запросов одновременно, чтобы гарантировать быстрое время отклика и повысить предсказуемость использования памяти. Пулы потоков должны быть в порядке, здесь.

Локальные переменные потока также могут быть аргументом для ваших собственных пулов потоков, но я не уверен, насколько это актуально на практике:

  • Создание нового потока с помощью std::thread начинается без инициализированных локальных переменных потока. Может быть, это не то, что вы хотите.
  • В потоках, порожденных async, мне несколько непонятно, потому что поток мог быть использован повторно. Насколько я понимаю, локальные переменные потока не гарантированно сбрасываются, но я могу ошибаться.
  • Использование собственных пулов потоков фиксированного размера, с другой стороны, дает вам полный контроль, если вам это действительно нужно.

Ответ 1

Вопрос 1:

Я изменил это с оригинала, потому что оригинал был неправ. У меня сложилось впечатление, что создание потока Linux было очень дешевым, и после тестирования я определил, что издержки вызова функции в новом потоке по сравнению с обычным потоком огромны. Затраты на создание потока для обработки вызова функции примерно в 10000 или более раз медленнее, чем простой вызов функции. Так что, если вы выполняете много небольших вызовов функций, пул потоков может быть хорошей идеей.

Совершенно очевидно, что стандартная библиотека C++, которая поставляется с g++, не имеет пулов потоков. Но я определенно вижу случай для них. Даже с учетом необходимости пересылки вызова через какую-то очередь между потоками, это, вероятно, будет дешевле, чем запуск нового потока. И стандарт позволяет это.

ИМХО, люди из ядра Linux должны работать над тем, чтобы сделать создание потоков дешевле, чем оно есть в настоящее время. Но стандартная библиотека C++ должна также рассмотреть возможность использования пула для реализации launch::async | launch::deferred launch::async | launch::deferred.

И OP является правильным, использование ::std::thread для запуска потока, конечно, вызывает создание нового потока вместо использования одного из пула. Поэтому ::std::async(::std::launch::async,...) является предпочтительным.

Вопрос 2:

Да, в основном это "неявно" запускает поток. Но на самом деле все еще совершенно очевидно, что происходит. Поэтому я не думаю, что слово неявно является особенно хорошим словом.

Я также не убежден, что принуждение вас ждать возвращения, прежде чем уничтожить, обязательно является ошибкой. Я не знаю, что вы должны использовать async вызов для создания потоков 'daemon', которые не должны возвращаться. И если ожидается, что они вернутся, не стоит игнорировать исключения.

Вопрос 3:

Лично мне нравится запуск потоков, чтобы быть явным. Я ценю острова, где вы можете гарантировать последовательный доступ. В противном случае вы получите изменчивое состояние, что вам всегда нужно где-то оборачивать мьютекс и не забывать его использовать.

Мне понравилась модель рабочей очереди намного лучше, чем модель "будущего", потому что вокруг лежат "островки серийного", чтобы вы могли более эффективно обрабатывать изменяемое состояние.

Но на самом деле, это зависит именно от того, что вы делаете.

Тест производительности

Итак, я проверил производительность различных методов вызова и пришел к этим цифрам в 8-ядерной системе (AMD Ryzen 7 2700X), работающей под управлением Fedora 29, скомпилированной с clang версии 7.0.1 и lib C++ (не libstd C++ ):

   Do nothing calls per second:   35365257                                      
        Empty calls per second:   35210682                                      
   New thread calls per second:      62356                                      
 Async launch calls per second:      68869                                      
Worker thread calls per second:     970415                                      

И родной, на моем MacBook Pro 15 "(Intel® Core ™ TM i7-7820HQ CPU @2,90 ГГц) с Apple LLVM version 10.0.0 (clang-1000.10.44.4) под OSX 10.13.6, я получаю следующее:

   Do nothing calls per second:   22078079
        Empty calls per second:   21847547
   New thread calls per second:      43326
 Async launch calls per second:      58684
Worker thread calls per second:    2053775

Для рабочего потока я запустил поток, затем использовал очередь без блокировки для отправки запросов другому потоку и затем дождался ответа "Готово", которое будет отправлено обратно.

"Ничего не делать" - это просто проверить нагрузку на тестовый жгут.

Понятно, что затраты на запуск потока огромны. И даже рабочий поток с очередью между потоками замедляет работу примерно в 20 раз на Fedora 25 в виртуальной машине и примерно на 8 на родной OS X.

Я создал проект Bitbucket, содержащий код, который я использовал для теста производительности. Его можно найти здесь: https://bitbucket.org/omnifarious/launch_thread_performance