Подтвердить что ты не робот

Глубокое понимание async/await на ASP.NET MVC

Я не понимаю точно, что происходит за кулисами, когда у меня есть асинхронное действие на контроллере MVC, особенно при работе с операциями ввода-вывода. Скажем, у меня есть действие для загрузки:

public async Task<ActionResult> Upload (HttpPostedFileBase file) {
  ....
  await ReadFile(file);

  ...
}

Из того, что я знаю, это основные шаги, которые происходят:

  • Новый поток просматривается из threadpool и назначается для обработки входящего запроса.

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

Мой вопрос: кто/когда/как это дожидается полной блокировки?

Примечание. Я видел сообщение в блоге Нет темы, и это имеет смысл для графических приложений, но для этого сценария на стороне сервера я не понимаю. На самом деле.

4b9b3361

Ответ 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 возвращает запрос и отправляет ответ.