Зачем использовать асинхронные запросы вместо большого пула потоков?

Во время Techdays здесь, в Нидерландах, Стив Сандерсон выступил с докладом о C#5, ASP.NET MVC 4 и асинхронной сети.

Он объяснил, что, когда запросы занимают много времени, все потоки из пула потоков становятся занятыми, и новые запросы должны ждать. Сервер не может справиться с нагрузкой, и все замедляется.

Затем он показал, как использование асинхронных веб-запросов повышает производительность, поскольку работа затем делегируется другому потоку, и пул потоков может быстро отвечать на новые входящие запросы. Он даже продемонстрировал это и показал, что 50 одновременных запросов сначала занимали 50 * 1 с, но при асинхронном поведении всего 1,2 с.

Но, увидев это, у меня остались вопросы.

  1. Почему мы не можем просто использовать больший пул потоков? Не используется ли async / await для запуска другого потока медленнее, чем просто увеличение пула потоков с самого начала? Это не похоже на то, что сервер, на котором мы работаем, вдруг получает больше потоков или что-то в этом роде?

  2. Запрос от пользователя все еще ожидает завершения асинхронного потока. Если поток из пула делает что-то еще, как поток "UI" остается занятым? Стив упомянул кое-что о "умном ядре, которое знает, когда что-то закончено". Как это работает?

3 ответа

Решение

Это очень хороший вопрос, и понимание его является ключом к пониманию того, почему асинхронный ввод-вывод так важен. Причина, по которой новая функция async/await была добавлена ​​в C# 5.0, заключается в упрощении написания асинхронного кода. Поддержка асинхронной обработки на сервере не нова, однако существует с ASP.NET 2.0.

Как показал Стив, при синхронной обработке каждый запрос в ASP.NET (и WCF) берет один поток из пула потоков. Проблема, которую он продемонстрировал, является хорошо известной проблемой, называемой "истощение пула потоков". Если вы выполняете синхронный ввод-вывод на вашем сервере, поток пула потоков останется заблокированным (ничего не делая) на время ввода-вывода. Поскольку существует ограничение на количество потоков в пуле потоков, при загрузке это может привести к ситуации, когда все потоки пула потоков блокируются в ожидании ввода-вывода, и запросы начинают помещаться в очередь, что приводит к увеличению времени отклика. Поскольку все потоки ожидают завершения ввода-вывода, вы увидите, что загрузка ЦП близка к 0% (даже несмотря на то, что время отклика идет вверх).

То, что вы спрашиваете (почему мы не можем просто использовать больший поток потоков?), Очень хороший вопрос. На самом деле, именно так большинство людей до сих пор решали проблему истощения пула потоков: просто добавьте больше потоков в пул потоков. Некоторая документация от Microsoft даже указывает, что в качестве исправления для ситуаций, когда может произойти голодание пула потоков. Это приемлемое решение, и до C# 5.0 это было намного проще, чем переписывать код, чтобы он был полностью асинхронным.

Однако есть несколько проблем с подходом:

  • Не существует значения, которое работает во всех ситуациях: количество потоков пула потоков, которое вам понадобится, линейно зависит от продолжительности ввода-вывода и нагрузки на ваш сервер. К сожалению, задержка ввода-вывода в большинстве случаев непредсказуема. Вот пример: допустим, вы отправляете HTTP-запросы стороннему веб-сервису в приложении ASP.NET, выполнение которого занимает около 2 секунд. Вы сталкиваетесь с истощением пула потоков, поэтому вы решили увеличить размер пула потоков, скажем, до 200 потоков, и затем он снова начнет работать нормально. Проблема в том, что, возможно, на следующей неделе у веб-службы возникнут технические проблемы, которые увеличат время отклика до 10 секунд. Внезапно истощение пула потоков вернулось, поскольку потоки блокируются в 5 раз дольше, поэтому теперь вам нужно увеличить число в 5 раз до 1000 потоков.

  • Масштабируемость и производительность. Вторая проблема заключается в том, что если вы сделаете это, вы все равно будете использовать один поток на запрос. Нити являются дорогим ресурсом. Каждому управляемому потоку в.NET требуется выделение 1 МБ памяти для стека. Для веб-страницы, выполняющей ввод-вывод, которая длится 5 секунд и при нагрузке 500 запросов в секунду, вам потребуется 2500 потоков в вашем пуле потоков, что означает 2,5 ГБ памяти для стеков потоков, которые будут бездействовать. Тогда у вас возникнет проблема переключения контекста, которая будет сильно влиять на производительность вашего компьютера (затрагивая все службы на компьютере, а не только ваше веб-приложение). Несмотря на то, что Windows неплохо справляется с игнорированием ожидающих потоков, она не предназначена для обработки такого большого количества потоков. Помните, что наибольшая эффективность достигается, когда количество запущенных потоков равно количеству логических процессоров на машине (обычно не более 16).

Таким образом, увеличение размера пула потоков - это решение, и люди делают это в течение десятилетия (даже в собственных продуктах Microsoft), оно просто менее масштабируемо и эффективно с точки зрения использования памяти и процессора, и вы всегда милость внезапного увеличения латентности ввода-вывода, которая может вызвать голод. Вплоть до C# 5.0 сложность асинхронного кода не стоила проблем для многих людей. async/await меняет все, как сейчас, вы можете воспользоваться масштабируемостью асинхронного ввода-вывода и одновременно писать простой код.

Более подробная информация: http://msdn.microsoft.com/en-us/library/ff647787.aspx "Используйте асинхронные вызовы для вызова веб-служб или удаленных объектов, когда есть возможность выполнить дополнительную параллельную обработку во время вызова веб-службы. По возможности избегайте синхронных (блокирующих) вызовов веб-служб, поскольку исходящие вызовы веб-службы выполняются с использованием потоков из пула потоков ASP.NET. Блокирующие вызовы уменьшают количество доступных потоков для обработки других входящих запросов".

  1. Async/await не основан на потоках; это основано на асинхронной обработке. Когда вы выполняете асинхронное ожидание в ASP.NET, поток запроса возвращается в пул потоков, поэтому нет потоков, обслуживающих этот запрос, пока не завершится асинхронная операция. Поскольку издержки запроса ниже, чем издержки потока, это означает, что async/await может масштабироваться лучше, чем пул потоков.
  2. Запрос имеет количество незавершенных асинхронных операций. Этот счетчик управляется реализацией ASP.NET SynchronizationContext, Вы можете прочитать больше о SynchronizationContext в моей статье MSDN - она ​​охватывает, как ASP.NET SynchronizationContext работает и как await использования SynchronizationContext,

ASP.NET асинхронная обработка была возможна до асинхронного /await - вы могли бы использовать асинхронные страницы и использовать компоненты EAP, такие как WebClient (Асинхронное программирование на основе событий - это стиль асинхронного программирования, основанный на SynchronizationContext). Async/await также использует SynchronizationContext, но имеет гораздо более простой синтаксис.

Представьте себе пул потоков как набор работников, которых вы наняли для выполнения своей работы. Ваши работники быстро запускают инструкции процессора для вашего кода.

Теперь ваша работа зависит от работы другого медленного парня; медленный парень - диск или сеть. Например, ваша работа может состоять из двух частей: одна часть должна выполняться до работы медленного парня и одна часть должна выполняться после работы медленного парня.

Как бы вы посоветовали своим работникам выполнять вашу работу? Не могли бы вы сказать каждому работнику: "Сделайте эту первую часть, затем подождите, пока этот медлительный парень не закончит, а затем выполните свою вторую часть"? Вы бы увеличили количество своих работников, потому что все они, кажется, ждут этого медленного парня, и вы не сможете удовлетворить новых клиентов? Нет!

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

Умное ядро, на которое вы ссылаетесь выше, - это способность операционных систем поддерживать такую ​​очередь для медленных сообщений о завершении ввода-вывода на диске и в сети.

Другие вопросы по тегам