Смотрю пример кода. Удивило, что сначала 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);
//будь дальше какой-то код, в контексте какого потока он выполнялся б?
};
}
Смотрите.
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;
)
Немного теории:
При использовании ключевого слова 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.
Вызов свойства Result
блокирует вызывающий поток, пока асинхронная операция GetContentAsync
не будет завершена.
Так в методе GetContentAsync
используется ключевое слово await
произойдет сохранение текущего SynchronizationContext
в данном случае контекста UI
.
После того, как метод GetContentAsync
выполнится, необходимо будет возобновить работу метода Button_Click
в сохраненном контексте SynchronizationContext
, но сделать этого не получится т.к. основной поток в режиме ожидания из-за вызова Result
.
Собственно резюме:
Если производительность стоит на первом месте или речь идет о библиотечном коде, которому безразлично, в каком потоке выполняться, следует использовать ConigureAwait(false)
.
Последующие вызывы ConfigureAwait(false)
никак не влияют на контекст синхронизации. Метод всё равно выполняется не UI-потоке.
Но я в своём коде тоже так поступаю. Это делается как правило хорошего тона. Чтобы в случае удаления одного из await
-конструкций метод не сломался.
Для упрощения работы с 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);
}
}
Первоначально при запуске TextBox пустой, если я начинаю в нем что то печатать, а потом удаляю все, чтобы он был пустой, то появляется ошибка валидацииНо...
На данный вопрос уже ответили:
Всем добрый деньПытаюсь отправить AJAX форму на сервер, но метод контроллера, которым должна обрабатываться форма, не вызывается