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
}

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

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

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

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

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

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