Использование ConfigureAwait(false)

196
14 октября 2018, 07:50

Смотрю пример кода. Удивило, что сначала ConfigureAwait(false) вызывается на httpClient.GetStringAsync, а затем на sourceStream.WriteAsync. Насколько я знаю ConfigureAwait(false) указывает, что код должен продолжать выполняться не в контексте UI, а в контексте таска. Зачем тогда 2 раза его вызывать?

private async void Button_Click(object sender, RoutedEventArgs e)
{
    HttpClient httpClient = new HttpClient();
    //до этого момента всё выполняется в UI контексте?
    string content = await httpClient.GetStringAsync("http://www.microsoft.com").
        ConfigureAwait(false); 
    //после выполнения верхней строчки остальной код который внизу будет выполняться в контексте веррхнего таска?
    using (FileStream sourceStream = new FileStream("temp.html", FileMode.Create, 
        FileAccess.Write, FileShare.None, 4096, useAsync: true))
    {
        byte[] encodedText = Encoding.Unicode.GetBytes(content);
        await sourceStream.WriteAsync(encodedText, 0, encodedText.Length).
            ConfigureAwait(false);
       //будь дальше какой-то код, в контексте какого потока он выполнялся б?
    };
}
Answer 1

Смотрите.

ConfigureAwait(false) означает, и правда, «мне всё равно, в каком потоке SynchronizationContext'е будет выполняться хвост метода».

То есть первый ConfigureAwait(false) может отправить «хвост» метода в фоновый поток. Но именно что может, а не должен! Если по какой-то причине первый таск выполнится синхронно (например, строка есть уже в кэше), то перевод в другой SynchronizationContext осуществлён не будет, и выполнение будет продолжаться в исходном контексте.

Если при этом второй await не снабжён конструкцией ConfigureAwait(false), то хвост метода будет выполняться снова-таки в исходном контексте — то есть, в вашем случае в контексте UI.

Таким образом, для библиотечных методов, которые не общаются с UI, практически необходимо к каждому внутреннему await'у добавлять ConfigureAwait(false).

Понятно, что дописывать к каждому из await'ов ConfigureAwait(false) немного лень. Можно вместо этого использовать такой трюк: «сбежать» на пул потоков в самом начале, и не беспокоиться об этом больше. Это можно сделать при помощи такой конструкции:

private async void Button_Click(object sender, RoutedEventArgs e)
{
    await AsyncHelper.RedirectToThreadPool();
    // всё, мы больше не в UI-контексте, гарантировано
    HttpClient httpClient = new HttpClient();
    string content = await httpClient.GetStringAsync("http://www.microsoft.com"); 
    // ...
}

Вспомогательные классы (взяты отсюда):

static class AsyncHelper
{
    public static ThreadPoolRedirector RedirectToThreadPool() =>
        new ThreadPoolRedirector();
}
public struct ThreadPoolRedirector : INotifyCompletion
{
    // awaiter и awaitable в одном флаконе
    public ThreadPoolRedirector GetAwaiter() => this;
    // true означает выполнять продолжение немедленно 
    public bool IsCompleted => Thread.CurrentThread.IsThreadPoolThread;
    public void OnCompleted(Action continuation) =>
        ThreadPool.QueueUserWorkItem(o => continuation());
    public void GetResult() { }
}

(идея взята из Stephen Toub await anything;)

Answer 2

Немного теории:

При использовании ключевого слова await компилятор делает много чего интересного, но в данном случае нас интересует, то что происходит запоминание (на самом деле запоминается и другие контексты) контекста синхронизации SynchronizationContext, который предназначен для исполнения кода в потоке конкретного вида. В классе SynchronizationContext есть важный метод Post, который гарантирует, что переданный делегат будет исполняться в правильном контексте.

Так вот, мы помним, что код, предшествующий первому await, исполняется в вызывающем потоке, но что происходит, когда исполнение вашего метода возобновляется после await? На самом деле, в большинстве случаев он также исполняется в вызывающем потоке, несмотря на то, что в промежутке вызывающий поток мог делать что-то еще. Для достижения такого эффекта текущий контекст SynchronizationContext сохраняется (это происходимит при встрече оператора await). Далее, когда метод возобновляется, компилятор вставляет вызов Post, чтобы исполнение возобновилось в запомненном контексте. Как правило, вызов этого метода обходится сравнительно дорого. Поэтому, чтобы избежать накладных расходов, .NET не вызывает Post, если запомненный контекст синхронизации совпадает с текущим на момент завершения задачи. Однако если контексты синхронизации различаются, то необходим дорогостоящий вызов Post. Если производительность стоит на первом месте или речь идет о библиотечном коде, которому безразлично, в каком потоке выполняться, то, возможно, не имеет смысла нести такие расходы. Поэтому, в таком случае следует вызвать метод ConigureAwait(false) перед тем как ждать его. Важно понимать, что данный метод задуман как способ информирования .NET о том, что вам безразлично, в каком потоке будет возобновлено выполнение. Если этот поток не очень важен, например взят из пула, то исполнение кода в нем и продолжится. Но если поток по какой-то причине важен, то .NET предпочтет освободить его для других дел, а исполнение вашего метода продолжить в потоке, взятом из пула. Решение о том, важен поток или нет, принимается на основе анализа текущего контекста синхронизации.

Это была вводная, а теперь слегка модернизируем ваш пример. Функционал, отвечающий за получение контента с сайта www.microsoft.com вынесем в отдельный метод. Обратите внимание, что ConigureAwait(false) здесь уже не используется.

 public async Task<string> GetContentAsync()
 {
     HttpClient httpClient = new HttpClient();
     string content = await httpClient.GetStringAsync("http://www.microsoft.com");
     return content;
 }

Далее слегка изменим обработчик события клик:

private async void Button_Click(object sender, RoutedEventArgs e)
{
    // Обратите внимание, что здесь мы не используем оператор `await`
    // Кроме того, все что идет ниже, нам уже не интересно, так как мы попали в deadlock
    var content = GetContentAsync().Result;
    using (FileStream sourceStream = new FileStream("temp.html", FileMode.Create, 
    FileAccess.Write, FileShare.None, 4096, useAsync: true))
    {
        byte[] encodedText = Encoding.Unicode.GetBytes(content);
        await sourceStream.WriteAsync(encodedText, 0, encodedText.Length).
        ConfigureAwait(false);
    };
}

Что же тут происходит и почему возникает deadlock.

  1. Вызов свойства Result блокирует вызывающий поток, пока асинхронная операция GetContentAsync не будет завершена.

  2. Так в методе GetContentAsync используется ключевое слово await произойдет сохранение текущего SynchronizationContext в данном случае контекста UI.

  3. После того, как метод GetContentAsync выполнится, необходимо будет возобновить работу метода Button_Click в сохраненном контексте SynchronizationContext, но сделать этого не получится т.к. основной поток в режиме ожидания из-за вызова Result.

Собственно резюме:

Если производительность стоит на первом месте или речь идет о библиотечном коде, которому безразлично, в каком потоке выполняться, следует использовать ConigureAwait(false).

Answer 3

Последующие вызывы ConfigureAwait(false) никак не влияют на контекст синхронизации. Метод всё равно выполняется не UI-потоке.

Но я в своём коде тоже так поступаю. Это делается как правило хорошего тона. Чтобы в случае удаления одного из await-конструкций метод не сломался.

Answer 4

Для упрощения работы с ConfigureAwait(false) Можно использовать

Fody ConfigureAwait

Your code

using Fody;
[ConfigureAwait(false)]
public class MyAsyncLibrary
{
    public async Task MyMethodAsync()
    {
        await Task.Delay(10);
        await Task.Delay(20);
    }
    public async Task AnotherMethodAsync()
    {
        await Task.Delay(30);
    }
}

What gets compiled

public class MyAsyncLibrary
{
    public async Task MyMethodAsync()
    {
        await Task.Delay(10).ConfigureAwait(false);
        await Task.Delay(20).ConfigureAwait(false);
    }
    public async Task AnotherMethodAsync()
    {
        await Task.Delay(30).ConfigureAwait(false);
    }
}
READ ALSO
HTML C# | Parser [закрыт]

HTML C# | Parser [закрыт]

Требуется спарсить данное значение из g_steamID средствами C#

163
Как выполнить валидацию по клику MVVM

Как выполнить валидацию по клику MVVM

Первоначально при запуске TextBox пустой, если я начинаю в нем что то печатать, а потом удаляю все, чтобы он был пустой, то появляется ошибка валидацииНо...

177
Отправка AJAX формы .Net MVC

Отправка AJAX формы .Net MVC

Всем добрый деньПытаюсь отправить AJAX форму на сервер, но метод контроллера, которым должна обрабатываться форма, не вызывается

168