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

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

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

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

Не думаю, что это навязывается стандартом, но IMHO, я ожидаю, что все хорошие реализации C ++ 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 версии?


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

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

К сожалению, текущий стандарт C ++ 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, это несколько неясно для меня, потому что поток мог быть использован повторно. Насколько я понимаю, локальные переменные потока не гарантированно сбрасываются, но я могу ошибаться.
  • С другой стороны, использование собственных (фиксированных) пулов потоков дает вам полный контроль, если вам это действительно нужно.

100

Решение

Вопрос 1:

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

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

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

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

вопрос 2:

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

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

Вопрос 3:

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

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

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

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

Итак, я проверил производительность различных методов вызова и получил эти цифры на двухпроцессорной виртуальной машине под управлением Fedora 25, скомпилированной с g ++ 6.3.1:


Do nothing calls per second: 30326536
Empty calls per second: 29348752
New thread calls per second: 15322
Async launch calls per second: 14779
Worker thread calls per second: 1357391

И родной, на моем MacBook Retina с Apple LLVM version 8.0.0 (clang-800.0.42.1) под OSX 10.12.3 я получаю это:


Do nothing calls per second: 20303610
Empty calls per second: 20222685
New thread calls per second: 40539
Async launch calls per second: 45165
Worker thread calls per second: 2662493

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

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

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

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

38

Другие решения

Других решений пока нет …

По вопросам рекламы [email protected]