Делает ли 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(); });

Questin 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
}

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

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

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

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

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

1 ответов


Вопрос 1:

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

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

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

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

Вопрос 2:

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

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

Вопрос 3:

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

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

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

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

Итак, я протестировал производительность различных методов вызова вещей и придумали эти номера на 2 CPU VM под управлением 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