[Перевод] Async/await в C#: подводные камни
Я бы хотел обсудить подводные камни, которые наиболее часто встречаются при работе с фичей async/await в C#, а также написать про то, как их можно обойти.Как работает async/awaitВнутренности async/await хорошо описаны Алексом Дэвисом в его книге, так что я только вкратце опишу их здесь. Рассмотрим следующий пример кода: public async Task ReadFirstBytesAsync (string filePath1, string filePath2) { using (FileStream fs1 = new FileStream (filePath1, FileMode.Open)) using (FileStream fs2 = new FileStream (filePath2, FileMode.Open)) { await fs1.ReadAsync (new byte[1], 0, 1); // 1 await fs2.ReadAsync (new byte[1], 0, 1); // 2 } } Эта функция читает по одному первому байту из двух файлов, пути к которым переданы через параметры. Что произойдет в строках »1» и »2»? Будут ли они выполнены параллельно? Нет. Эта функция будет «разбита» ключевым словом «await» на три части: часть, предшествующая »1», часть между »1» и »2» и часть, следующая за »2».
Функция запустит новый I/O bound поток в строке »1», передаст ему вторую часть себя же (ту часть, которая между »1» и »2») в качестве callback-а и возвратит управление. После того как I/O поток завершит работу, будет вызван callback, и метод продолжит выполнение. Метод создаст еще один I/O поток в строке »2», передаст ему третью часть себя в качестве callback-а и опять возвратит управление. После того как второй I/O поток завершит выполнение, будет запущена остальная часть метода.
Магия здесь присутствует благодаря компилятору, который преобразует методы, помеченные ключевым словом «async» в конечный автомат, по аналогии с тем, как он преобразует методы-итераторы.
Когда использовать async/await? Существуют два основных сценария, в которых использование async/await предпочтительно.В первую очередь, эта фича может быть использована в толстых клиентах для предоставления пользователям лучшего user experience. Когда пользователь нажимает на кнопку, стартуя тяжелую вычислительную операцию, наилучшим выходом будет выполнить эту операцию асинхронно, без блокировки UI потока. До .NET 4.5 подобная логика требовала гораздо больших усилий. Теперь ее можно запрограммировать примерно так:
private async void btnRead_Click (object sender, EventArgs e) { btnRead.Enabled = false; using (FileStream fs = new FileStream («File path», FileMode.Open)) using (StreamReader sr = new StreamReader (fs)) { Content = await sr.ReadToEndAsync (); } btnRead.Enabled = true; } Обратите внимание, что флаг Enabled в обоих случаях устанавливается UI-потоком. Этот подход устраняет необходимость написания такого некрасивого кода:
if (btnRead.InvokeRequired) { btnRead.Invoke ((Action)(() => btnRead.Enabled = false)); } else { btnRead.Enabled = false; } Другими словами, весь «легкий» код выполняется вызывающим потоком, в то время как «тяжелые» части делегируются отдельному потому (I/O или CPU-bound). Такой подход позволяет существенно сократить количество усилий, необходимых для синхронизации доступа к UI элементам, т.к. управление ими происходит только из UI потока.
Во-вторых, async/await может быть использован в веб-приложениях для лучшей утилизации потоков. Команда ASP.NET MVC сделала асинхронные контроллеры очень простыми в имплементации. Вы можете просто написать action-метод как на примере ниже и ASP.NET сделает всю остальную работу:
public class HomeController: Controller
{
public async Task
Async/await в C#: подводные камни Если вы разрабатываете стороннюю библиотеку, очень важно всегда настраивать await таким образом, чтобы остальная часть метода была выполнена произвольным потоком из пула. Другими словами, в коде сторонних библиотек всегда необходимо добавлять ConfigureAwait (false).В первую очередь, сторонние библиотеки обычно не работают с UI контролами (если конечно это не UI библиотека), поэтому нет никакой необходимости связывать UI поток. Вы можете немного увеличить производительность если позволите CLR выполнять ваш код любым потоком из пула. Во-вторых, используя дефолтную имплементацию (или явно проставляя ConfigureAwait (true)), вы оставляете потенциальную дыру для дедлоков. Рассмотрим следующий пример:
private async void button1_Click (object sender, EventArgs e)
{
int result = DoSomeWorkAsync ().Result; // 1
}
private async Task
ASP.NET ведет себя таким же образом. Несмотря на то, что в ASP.NET нет выделенного UI потока, код в action-ах котроллеров не может выполняться более чем одним рабочим потоком одновременно.
Конечно, мы можем использовать await вместо обращения к свойству Result для того, чтобы избежать дедлока:
private async void button1_Click (object sender, EventArgs e)
{
int result = await DoSomeWorkAsync ();
}
private async Task
@Html.Action («SomeAction», «SomeController») Пользователи ваших библиотек как правило не имеют прямого доступа к коду этих библиотек, поэтому всегда заблаговременно проставляйте ConfigureAwait (false) в ваших асинхронных методах.
Как не нужно использовать PLINQ и async/await
Рассмотрим пример:
private async void button1_Click (object sender, EventArgs e)
{
btnRead.Enabled = false;
string content = await ReadFileAsync ();
btnRead.Enabled = true;
}
private Task
Что происходит здесь? Вместо того, чтобы создать единственный I/O поток, мы создаем и CPU поток на строке »1», и I/O поток на строке »2». Это пустая трата потоков. Чтобы исправить ситуацию, нам нужно использовать асинхронную версию метода Read:
private Task
public void SendRequests () { _urls.AsParallel ().ForAll (url => { var httpClient = new HttpClient (); httpClient.PostAsync (url, new StringContent («Some data»)); }); } Выглядит так, будто мы отправляем запросы параллельно, не так ли? Да, это так, но здесь мы имеем ту же проблему, что в предыдущем примере: вместо того, чтобы создать единственный I/O поток, мы создаем и I/O, и CPU-bound поток для каждого запроса. Исправить ситуацию можно используя метод Task.WaitAll:
public void SendRequests ()
{
IEnumerable
Ссылка на оригинал статьи: Async/await in C#: pitfalls