Асинхронность в C# 5. Часть 2: Откуда await?

Сперва я хочу прояснить два момента, поскольку наше исследование простоты использования показало, что это может быть непонятным. Помните код, который я показывал вам в прошлый раз?

 async void ArchiveDocuments(List<Url> urls)
{
  Task archive = null;
  for(int i = 0; i < urls.Count; ++i)
  {
    var document = await FetchAsync(urls[i]);
    if (archive != null)
      await archive;
    archive = ArchiveAsync(document);
  }
}

Два момента:

1) Модификатор метода “async” не означает, что «этот метод будет автоматически вызываться асинхронно в рабочем потоке». Он означает противоположное; это значит «этот метод содержит поток выполнения, включающий ожидание завершения асинхронных операций и будет, таким образом, переделан компилятором в стиль передачи продолжений, что позволит асинхронным операциям продолжить выполнение этого метода с нужной точки». Основной смысл методов с модификатором async заключается в том, что вы можете находиться в текущем потоке настолько долго, насколько это возможно. Эти методы сродни продолжениям: они предоставляют однопоточную кооперативную многозадачность в языке C#. (Позднее мы обсудим причины, которые требуют наличия модификатора async вместо выведения его компилятором.)

2) Оператор “await” используется дважды, но это не означает, что «этот метод блокирует текущий поток до окончания выполнения асинхронной операции». Это бы сделало асинхронную операцию опять синхронной, а это именно то, чего мы пытаемся избежать. Скорее это означает противоположное: «если ожидаемая нами задача еще не завершилась, то превратить остаток текущего метода в продолжение текущей задачи, и затем немедленно вернуть управление вызывающему коду; после завершения задачи, она вызовет продолжение самостоятельно» .

К сожалению, при первом взгляде на контекстные ключевые слова “async” и “await”, большинству людей интуиция подсказывает противоположное значение. Однако многие попытки найти более подходящие ключевые слова закончились неудачно. Если у вас есть мысли об именах ключевых слов или о комбинации ключевых слов, которые будут краткими, звучными, и не будут сбивать с толку, я с радостью их выслушаю. Вот некоторые наши идеи, которые мы отбросили по различным причинам:

 wait for FetchAsync(…)
yield with FetchAsync(…)
yield FetchAsync(…)
while away the time FetchAsync(…)
hearken unto FetchAsync(…)
for sooth Romeo wherefore art thou FetchAsync(…)

Идем дальше. Нам еще о многом стоит сказать. Следующее, о чем я хочу рассказать, это «что это за штуки, о которых я упомянул в прошлый раз?»

В прошлый раз я упомянул, что следующее выражение языка C# 5.0

 document = await FetchAsync(urls[i])

аналогично следующему:

 state = State.AfterFetch;
fetchThingy = FetchAsync(urls[i]);
if (fetchThingy.SetContinuation(archiveDocuments))
  return; 
AfterFetch: ;
document = fetchThingy.GetValue();

а где же Thingy?

В нашей модели асинхронные методы обычно возвращают Task<T>; давайте пока предположим, что метод FetchAsync возвращает Task<Document>. (О причинах использования «Шаблона на основе задач» я расскажу в следующих сообщениях). Реальный код будет таким:

 fetchAwaiter = FetchAsync(urls[i]).GetAwaiter();
state = State.AfterFetch;
if (fetchAwaiter.BeginAwait(archiveDocuments))
  return;
AfterFetch: ;
document = fetchAwaiter.EndAwait();

Вызов метода FetchAsync создает и возвращает объект Task<Document>, т.е. объект, представляющий собой работающую задачу. Вызов этого метода возвращает Task<Document> немедленно, а уже затем асинхронно получает этот документ каким-то образом. Возможно, вся остальная работа выполняется в другом потоке, или путем помещения элемента в очередь сообщений Windows с последующей проверкой очереди в моменты бездействия, в общем, не важно. Это дело этого метода. Мы знаем, что что-то должно произойти после его завершения. (Я расскажу об однопоточной асинхронности в следующих сообщениях).

Чтобы что-то произошло после завершения метода, мы получаем экземпляр класса Awaiter данной задачи, который содержит два метода. BeginAwait получает продолжение текущей задачи; после завершения задачи происходит чудо и каким-то образом вызывается продолжение. (О том, что именно там происходит, я также расскажу в другой раз). Если метода BeginAwait возвращает true, тогда продолжение будет вызвано; в противном случае задача уже завершена и необходимости в механизме продолжения просто нет.

Метод EndAwait получает результаты завершенной задачи.

Мы представим реализации методов BeginAwait и EndAwait с классами Task (для задач, которые логически возвращают void) и Task<T> (для задач, с возвращаемыми значениями). Но как быть с асинхронными методами, которые не возвращают объекты типа Task или Task<T>? В этом случае мы собираемся использовать тот же подход, что и в LINQ. В LINQ, если вы напишите:

 from c in customers where c.City == "London" blah blah blah

То компилятор преобразует это в:

 customers.Where(c=>c.City=="London") …

А механизм разрешения перегрузки (overload resolution) попытается найти наиболее подходящий метод Where, проверяя, не реализует ли его класс Customer, или, подходящий метод расширения. Для методов GetAwaiter / BeginAwait / EndAwait будет использован аналогичный образец; мы просто будем стараться найти подходящий метод для преобразованного выражения. И если будет нужно вызвать метод расширения, мы вызовем именно его.

Итак, в конце концов: почему “Task”?

Причина заключается в том, что асинхронность не требует наличие параллелизма, а параллелизм не требует асинхронности, и многие инструменты полезные для параллелизма могут быть также полезными для непараллельной асинхронности. В классе Task ничего не сказано об обязательном параллелизме; тот факт, что Task Parallel Library использует шаблон на основе задачи для представления единицы текущей работы, которая может быть распараллелена, не требует многопоточности.

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

В класс Task из TPL уже сделаны серьезные вложения; он поддерживает механизм отмены и другие полезные возможности. Вместо изобретения чего-то нового, вроде нового типа “IFuture”, мы можем просто расширить существующий код, для решения задач асинхронности.

В следующий раз: как далее компоновать асинхронные задачи.

Оригинал статьи