Как асинхронный w/ждущий отличается от синхронного вызова?

Я читал об асинхронных вызовах функций на http://msdn.microsoft.com/en-us/library/vstudio/hh191443.aspx?cs-save-lang=1&cs-lang=csharp.

В первом примере они делают это, и я получаю:

Task<string> getStringTask = client.GetStringAsync("http://msdn.microsoft.com");

// You can do work here that doesn't rely on the string from GetStringAsync.
DoIndependentWork();

string urlContents = await getStringTask;

Но тогда они объясняют, что если не будет никакой работы, которая будет сделана в среднем, вы можете просто сделать это следующим образом:

string urlContents = await client.GetStringAsync();

Из того, что я понимаю, ключевое слово await приостанавливает поток кода до тех пор, пока функция не вернется. Итак, как это отличается от:

string urlContents = client.GetString();

?

Ответ 1

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

Если вы просто вызываете client.GetString(), выполнение потока не будет продолжаться до тех пор, пока этот метод не завершит выполнение, что заблокирует поток и может привести к тому, что пользовательский интерфейс перестанет отвечать.

Пример:

public void MainFunc()
{
    InnerFunc();
    Console.WriteLine("InnerFunc finished");
}

public void InnerFunc()
{
    // This causes InnerFunc to return execution to MainFunc,
    // which will display "InnerFunc finished" immediately.
    string urlContents = await client.GetStringAsync();

    // Do stuff with urlContents
}

public void InnerFunc()
{
    // "InnerFunc finished" will only be displayed when InnerFunc returns,
    // which may take a while since GetString is a costly call.
    string urlContents = client.GetString();

    // Do stuff with urlContents
}

Ответ 2

Из того, что я понимаю, ключевое слово await приостанавливает поток кода, пока функция не вернет

Ну, да и нет.

  • Да, потому что поток кода в каком-то смысле останавливается.
  • Нет, потому что поток, выполняющий этот поток кода, не блокируется. (Синхронный вызов client.GetString() заблокирует поток).

Фактически, он вернется к своему вызывающему методу. Чтобы понять, что это значит, вернувшись к его вызывающему методу, вы можете прочитать о другом мастере компилятора С# - инструкции yield return.

Блоки Iterator с yield return будут разбивать метод на конечный автомат, где код после оператора yield return будет выполняться только после вызова MoveNext() в перечислителе. (Смотрите this и this).

Теперь механизм async/await также основан на аналогичной машине состояний (однако, это намного сложнее, чем машина состояния yield return).

Чтобы упростить дело, рассмотрим простой метод async:

public async Task MyMethodAsync()
{
    // code block 1 - code before await

    // await stateement
    var r = await SomeAwaitableMethodAsync();

    // code block 2 - code after await
}
  • Когда вы отмечаете метод с идентификатором async, вы говорите компилятору разбить метод на конечный автомат и что вы собираетесь await внутри этого метода.
  • Допустим, что код работает в потоке Thread1, и ваш код вызывает этот MyMethodAsync(). Затем code block 1 будет синхронно работать в одном потоке.
  • SomeAwaitableMethodAsync() также будет вызываться синхронно, но позволяет сказать, что метод запускает новую асинхронную операцию и возвращает Task.
  • Это когда await входит в изображение. Он вернет поток кода обратно своему вызывающему, и поток Thread1 может свободно запускать код вызывающих абонентов. То, что происходит тогда в методе вызова, зависит от того, является ли метод вызова await на MyMethodAsync() или что-то еще, но важная вещь Thread1 не заблокирована.
  • Теперь остальная магия ожидания - Когда задача, возвращаемая SomeAwaitableMethodAsync(), в конечном итоге завершается, code block 2 запускается по расписанию.
  • async/await построен на параллельной библиотеке задач - поэтому это планирование выполняется по TPL.
  • Теперь дело в том, что этот code block 2 не может быть запланирован по тому же потоку Thread1, если только он не имеет активного SynchronizationContext с аффинностью потока (например, поток пользовательского интерфейса WPF/WinForms). await имеет значение SynchronizationContext, поэтому code block 2 запланирован на тот же SynchronizationContext, если он есть, когда был вызван MyMethodAsync(). Если активного SynchronizationContext не было, то при любой возможности code block 2 будет работать через несколько потоков.

Наконец, я скажу, что поскольку async/await основан на машине состояний, созданной компилятором, например, yield return, он разделяет некоторые из недостатков - например, вы не можете await внутри блока finally.

Надеюсь, это уберет ваши сомнения.