Это слабо связано с вопросом: объединены ли 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
, для меня это несколько непонятно, потому что поток можно было использовать повторно. Насколько я понимаю, сброс локальных переменных потока не гарантируется, но я могу ошибаться. - С другой стороны, использование собственных пулов потоков (фиксированного размера) дает вам полный контроль, если он вам действительно нужен.
источник
std::async(launch::async)
похоже, у них гораздо больше шансов быть объединенными». Нет, я считаю,std::async(launch::async | launch::deferred)
что это можно объединить. С помощью всего лишьlaunch::async
задача , как предполагается , будет запущен в новом потоке , независимо от того, что другие задачи выполняются. С политикойlaunch::async | launch::deferred
реализация может выбрать, какая политика, но, что более важно, откладывает выбор какой политики. То есть он может дождаться, пока поток в пуле потоков не станет доступным, а затем выбрать политику асинхронности.std::async()
. Мне все еще любопытно посмотреть, как они поддерживают нетривиальные деструкторы thread_local в пуле потоков.launch::async
то он обрабатывает ее так, как если бы она была единственной,launch::deferred
и никогда не выполняет ее асинхронно - по сути, эта версия libstdc ++ "выбирает" всегда использовать отложенный, если не указано иное.std::async
могла бы быть прекрасной вещью для производительности - это могла бы быть стандартная система выполнения краткосрочных задач, естественно поддерживаемая пулом потоков. Прямо сейчас это просто какая-std::thread
то чушь, чтобы заставить функцию потока возвращать значение. Да, и они добавили избыточную «отложенную» функциональность, которая полностью перекрывает работуstd::function
.Ответы:
Вопрос 1 :
Я изменил это с оригинала, потому что оригинал был неправильным. У меня создалось впечатление, что создание потока Linux было очень дешевым, и после тестирования я определил, что накладные расходы на вызов функции в новом потоке по сравнению с обычным огромны. Накладные расходы на создание потока для обработки вызова функции примерно в 10000 или более раз медленнее, чем простой вызов функции. Итак, если вы выполняете много небольших вызовов функций, пул потоков может быть хорошей идеей.
Совершенно очевидно, что стандартная библиотека C ++, поставляемая с g ++, не имеет пулов потоков. Но я определенно вижу для них дело. Даже с накладными расходами, связанными с проталкиванием вызова через какую-то межпотоковую очередь, это, вероятно, будет дешевле, чем запуск нового потока. И стандарт это позволяет.
IMHO, люди, работающие с ядром Linux, должны работать над тем, чтобы сделать создание потоков дешевле, чем сейчас. Но стандартная библиотека C ++ также должна рассмотреть возможность использования пула для реализации
launch::async | launch::deferred
.И OP правильный, использование
::std::thread
для запуска потока, конечно, заставляет создание нового потока вместо использования одного из пула. Так::std::async(::std::launch::async, ...)
что предпочтительнее.Вопрос 2 :
Да, в основном это «неявно» запускает поток. Но на самом деле, все еще совершенно очевидно, что происходит. Так что я не думаю, что это слово неявно является особенно хорошим словом.
Я также не уверен, что заставлять вас ждать возврата до уничтожения обязательно является ошибкой. Я не знаю, следует ли использовать этот
async
вызов для создания потоков «демонов», которые не должны возвращаться. И если ожидается, что они вернутся, игнорировать исключения нельзя.Вопрос 3 :
Лично мне нравится явный запуск потоков. Я очень ценю острова, где вы можете гарантировать последовательный доступ. В противном случае вы получите изменяемое состояние, когда вам всегда нужно где-то оборачивать мьютекс и не забывать его использовать.
Мне понравилась модель очереди работы намного больше, чем модель «будущего», потому что есть «островки серийного номера», так что вы можете более эффективно обрабатывать изменяемое состояние.
Но на самом деле это зависит от того, что именно вы делаете.
Тест производительности
Итак, я протестировал производительность различных методов вызова вещей и получил эти числа на 8-ядерной (AMD Ryzen 7 2700X) системе с Fedora 29, скомпилированной с clang версии 7.0.1 и libc ++ (не libstdc ++):
И родной, на моем MacBook Pro 15 дюймов (Intel (R) Core (TM) i7-7820HQ CPU @ 2.90GHz) с
Apple LLVM version 10.0.0 (clang-1000.10.44.4)
OSX 10.13.6, я получаю следующее:Для рабочего потока я запустил поток, затем использовал очередь без блокировки для отправки запросов другому потоку, а затем дождался ответа «Готово», который будет отправлен обратно.
«Ничего не делать» - это просто проверка верхней части тестового ремня.
Понятно, что накладные расходы на запуск потока огромны. И даже рабочий поток с межпотоковой очередью замедляет работу примерно в 20 раз на Fedora 25 на виртуальной машине и примерно в 8 раз на собственной OS X.
Я создал проект Bitbucket, содержащий код, который я использовал для теста производительности. Его можно найти здесь: https://bitbucket.org/omnifarious/launch_thread_performance
источник