Ответ 1
Вступительное замечание: теперь я узнал об этом намного больше и поэтому переписал свой ответ. Спасибо @super, @MM и (последним) @DavidHaim и @NoSenseEtAl за то, что поставили меня на правильный путь.
tl; dr реализация Microsoft std::async
несоответствует, но у них есть свои причины, и то, что они сделали, действительно может быть полезно, как только вы это правильно поймете.
Для тех, кто этого не хочет, не сложно записать замену замены для std::async
которая работает одинаково на всех платформах. Я разместил его здесь.
Edit: Вау, как открытые MS находятся в эти дни, мне это нравится, см. Https://github.com/MicrosoftDocs/cpp-docs/issues/308
Позвольте быть в начале. cppreference имеет это сказать (акцент и зачеркнуть мое):
Функция
async
шаблона выполняет функциюf
асинхронно (возможно,необязательно в отдельном потоке, который может быть частью пула потоков).
Тем не менее, стандарт C++ говорит следующее:
Если
launch::async
задан вpolicy
, [std::async
] вызывает [функцию f], как если бы в новом потоке выполнения...
Итак, что правильно? Эти два утверждения имеют очень разную семантику, как обнаружил ОП. Ну, конечно, стандарт верен, так как и clang, и gcc show, так почему же реализация Windows отличается? И, как и многие вещи, это сводится к истории.
(Старинная) ссылка, которую выровняли MM, имеет это, среди прочего:
... У Microsoft есть реализация [
std::async
] в виде PPL (Parallel Pattern Library)... [и] Я могу понять стремление этих компаний к изгибу правил и сделать эти библиотеки доступными черезstd::async
, особенно если они могут значительно повысить производительность...... Microsoft захотела изменить семантику
std::async
при вызове с помощьюlaunch_policy::async.
Я думаю, что это было в значительной степени исключено в последующем обсуждении... (следует логическое обоснование, если вы хотите узнать больше, а затем прочитать ссылку, это того стоит).
И PPL основан на встроенной поддержке Windows для ThreadPools, поэтому @super был прав.
Итак, что делает пул потоков Windows и для чего он хорош? Ну, он предназначен для эффективного управления часто выполняемыми, краткосрочными задачами эффективным образом, так что точка 1 не злоупотребляет, но мои простые тесты показывают, что если это ваш прецедент, то он может предложить значительную эффективность. Это, по сути, две вещи
- Он перерабатывает потоки, вместо того, чтобы всегда запускать новый для каждой асинхронной задачи, которую вы запускаете.
- Он ограничивает общее количество используемых им потоков фона, после чего вызов
std::async
будет блокироваться до тех пор, пока поток не станет свободным. На моей машине это число составляет 768.
Поэтому, зная все это, мы теперь можем объяснить наблюдения ОП:
-
Создается новый поток для каждой из трех задач, запущенных
main()
(потому что ни одно из них не завершается немедленно). -
Каждый из этих трех потоков создает новую локальную переменную
Foo some_thread_var
. -
Эти три задачи выполняются до завершения, но потоки, в которых они работают, остаются в силе (сон).
-
Затем программа засыпает на короткое время, а затем завершает работу, оставляя 3 ните-локальные переменные неразрушаемыми.
Я провел несколько тестов, и в дополнение к этому я нашел несколько ключевых моментов:
- Когда поток перерабатывается, локальные переменные потока повторно используются. В частности, они не уничтожаются, а затем воссоздаются (вы были предупреждены!).
- Если все асинхронные задачи завершены и вы достаточно долго ждете, пул потоков завершает все связанные потоки, а затем локальные переменные потока уничтожаются. (Без сомнения, фактические правила более сложны, чем то, что я наблюдал).
- По мере ввода новых асинхронных задач пул потоков ограничивает скорость создания новых потоков в надежде, что он станет бесплатным, прежде чем ему понадобится выполнить все эти работы (создание новых потоков является дорогостоящим). Поэтому вызов
std::async
может занять некоторое время (до 300 мс в моих тестах). Тем временем, он просто висит вокруг, надеясь, что его корабль войдет. Это поведение задокументировано, но я называю это здесь, если он вас удивит.
Выводы:
-
Реализация
std::async
для Microsoft является несоответствующей, но она четко разработана с определенной целью, и эта цель заключается в том, чтобы эффективно использовать API WinPool Win32. Вы можете избивать их за благосклонно пренебрегая стандартом, но так долго, и у них, вероятно, есть (важные!) Клиенты, которые полагаются на него. Я попрошу их назвать это в своей документации. Не делать это преступно. -
Это не безопасно использовать thread_local переменных в
std::async
задач на Windows. Просто не делай этого, это закончится слезами.