Глубокое понимание async/await на ASP.NET MVC
Я не понимаю точно, что происходит за кулисами, когда у меня есть асинхронное действие на контроллере MVC, особенно при работе с операциями ввода-вывода. Скажем, у меня есть действие для загрузки:
public async Task<ActionResult> Upload (HttpPostedFileBase file) {
....
await ReadFile(file);
...
}
Из того, что я знаю, это основные шаги, которые происходят:
-
Новый поток просматривается из threadpool и назначается для обработки входящего запроса.
-
Когда ожидание попадает, если вызов является операцией ввода-вывода, исходный поток возвращается в пул, а элемент управления передается на так называемый IOCP (порт завершения ввода вывода). Я не понимаю, почему запрос все еще жив и ждет ответа, потому что в конечном итоге вызывающий клиент будет ждать завершения нашего запроса.
Мой вопрос: кто/когда/как это дожидается полной блокировки?
Примечание. Я видел сообщение в блоге Нет темы, и это имеет смысл для графических приложений, но для этого сценария на стороне сервера я не понимаю. На самом деле.
Ответы
Ответ 1
Есть несколько хороших ресурсов в сети, которые описывают это подробно. Я написал статью MSDN, которая описывает это на высоком уровне.
Я не понимаю, почему запрос все еще жив и ждет ответа, потому что в конечном итоге вызывающий клиент будет ждать завершения нашего запроса.
Он все еще жив, потому что время выполнения ASP.NET еще не завершено. Выполнение запроса (путем отправки ответа) является явным действием; это не похоже на то, что запрос будет выполнен сам по себе. Когда ASP.NET видит, что действие контроллера возвращает Task
/Task<T>
, он не выполнит запрос до завершения этой задачи.
Мой вопрос: кто/когда/как это дожидается полной блокировки?
Ничего не ждет.
Подумайте об этом так: ASP.NET имеет набор текущих запросов, которые он обрабатывает. Для данного запроса, как только он будет завершен, ответ отправляется, а затем этот запрос удаляется из коллекции.
Ключ состоит в том, что он представляет собой набор запросов, а не потоков. Каждый из этих запросов может иметь или не иметь поток, работающий над ним в любой момент времени. Синхронные запросы всегда имеют один поток (тот же поток). Асинхронные запросы могут иметь периоды, когда у них нет потоков.
Примечание. Я видел эту тему: http://blog.stephencleary.com/2013/11/there-is-no-thread.html, и это имеет смысл для приложений GUI, но для этого сценария на стороне сервера я не понимаю.
Беспотенциальный подход к I/O работает точно так же для приложений ASP.NET, как и для графических приложений.
В конце концов, запись файла завершится, что (в конечном итоге) завершит задачу, возвращенную из ReadFile
. Эта работа "завершение задачи" обычно выполняется с потоком пула потоков. Поскольку задача теперь завершена, действие Upload
будет продолжено, заставив этот поток войти в контекст запроса (то есть, теперь поток выполняет этот запрос снова). Когда метод Upload
завершен, задача, возвращаемая из Upload
, завершена, и ASP.NET выписывает ответ и удаляет запрос из своей коллекции.
Ответ 2
Под капотом компилятор выполняет ловкость руки и преобразует ваш код async
\await
в код Task
с обратным вызовом. В самом простом случае:
public async Task X()
{
A();
await B();
C();
}
Возвращается к чему-то вроде:
public Task X()
{
A();
return B().ContinueWith(()=>{ C(); })
}
Итак, нет волшебства - всего много Task
и обратных вызовов. Для более сложного кода преобразования будут более сложными, но в итоге полученный код будет логически эквивалентен тому, что вы написали. Если вы хотите, вы можете взять один из ILSpy/Reflector/JustDecompile и сами убедиться, что скомпилировано "под капотом".
ASP.NET MVC-инфраструктура, в свою очередь, достаточно интеллектуальна, чтобы узнать, является ли ваш метод действий обычным, или основан на Task
, и изменит его поведение по очереди. Поэтому запрос не "исчезает".
Одно распространенное заблуждение состоит в том, что все с async
порождает другой поток. Фактически, это в основном противоположное. В конце длинной цепочки методов async Task
обычно есть метод, который выполняет некоторую асинхронную операцию ввода-вывода (например, чтение с диска или связь через сеть), что является волшебной вещью, выполняемой самой Windows. На протяжении всей этой операции нет никакого потока, связанного с кодом, - он фактически останавливается. Однако после завершения операции Windows перезвонит, а затем поток из пула потоков назначен для продолжения выполнения. Там немного кода рамки, чтобы сохранить HttpContext
запроса, но все.
Ответ 3
Время выполнения ASP.NET понимает, какие задачи и задерживает отправку ответа HTTP, пока задача не будет выполнена. Фактически значение Task.Result
необходимо для того, чтобы даже генерировать ответ.
Среда выполнения в основном делает это:
var t = Upload(...);
t.ContinueWith(_ => SendResponse(t));
Итак, когда ваш await
попадает в ваш код, а код времени выполнения выходит из стека и "нет потока" в этот момент. Обратный вызов ContinueWith
возвращает запрос и отправляет ответ.