Реальная проблема не в «пуле потоков против async
/ await
», а в синхронном вводе-выводе и асинхронном вводе-выводе. [1]
В Windows потоки являются относительно дорогими объектами - с потоком связано много бухгалтерии ОС, не говоря уже о 1 МБ предварительно выделенного стекового пространства. Это накладывает довольно низкий предел на количество потоков, которые система будет поддерживать, и даже если вы не достигнете этого предела, переключение контекста между всеми этими потоками также не дешево. Это ограничение в 32 000 потоков является строго теоретическим и очень оптимистичным в отношении того, сколько потоков вы можете иметь и при этом быть отзывчивыми! [2]
Введите асинхронный ввод-вывод, который оптимизирован для использования только необходимого количества потоков (обычно это несколько консервативных кратных количеству ядер физических процессоров в системе), в идеале даже не создавая новых потоков за пределами первоначального пакета. Эти потоки предназначены для обработки завершенных операций ввода-вывода путем удаления их из очереди (известной как порт завершения). Пока выполняется асинхронная операция, никакой нити ей не выделено, даже как элемент в списке ожидания (у Стивена Клири есть хороший пост в блоге , в котором это объясняется более подробно). Не нужно много воображения, чтобы подумать о том, что более эффективно:
- Несколько тысяч отдельных потоков, каждый из которых ожидает определенной операции, которые должны быть активированы и переключаться на (и между) в зависимости от того, какие операции завершены; или
- Несколько десятков потоков (если таковые имеются), каждый из которых может обрабатывать любую завершенную операцию, так что только столько из них когда-либо нужно запускать, сколько необходимо для реагирования.
Как оказалось, последний масштабируется намного лучше, чем первый; модель "поток на запрос", которая является обычной в наивном серверном коде, быстро показывает свои пределы, даже когда вы используете пул потоков для сокращения создания новых потоков. Обратите внимание, что это было проблемой задолго до того, как async
/ await
когда-либо был, а также решение, которое использовала Windows; async
/ await
- это просто новый способ написания кода для использования существующих механизмов.
Вы, вероятно, заметите разницу только с несколькими запросами в полете за раз? Нет. Но поскольку async
/ await
, по сути, позволяет вам писать код, который выглядит синхронно, но имеет масштабируемость асинхронного ввода-вывода «бесплатно», почему бы вам не выбрать, чтобы использовать это в пользу синхронного ввода-вывода в очередь в пул потоков?
[1] Оказывается, Стивен Клири уже написал большую часть того, что в этом ответе несколько лет назад. Я также рекомендую вам прочитать это.
[2] Вот старый пост Марка Руссиновича, где он на самом деле пытается выжать как можно больше потоков из системы - просто для удовольствия и для получения прибыли. Он «только» добирается до 55K на 64-битной машине до того, как все ресурсы уйдут, и это с регулировкой размера стека по умолчанию и без какой-либо действительно полезной работы. В современной системе вы, вероятно, могли бы получить больше, но реальный вопрос не должен заключаться в том, «сколько потоков я могу иметь» - если это так, вы делаете это неправильно.