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

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

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

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

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

  • Почему мы не можем использовать больше threadpool? Не использует async/await, чтобы вызвать еще один поток медленнее, чем просто увеличить threadpool с самого начала? Не похоже, что сервер, на котором мы запускаем, вдруг получает больше потоков или что-то еще?

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

Ответ 1

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

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

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

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

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

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

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

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

Ответ 2

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

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

Ответ 3

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

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

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

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

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