Асинхронные операции async / await, великолепный синтаксический сахар C #, делают асинхронное программирование красивым и простым в реализации. Даже JavaScript заимствует синтаксис async / await, чтобы сделать код JavaScript, наполненный обратными вызовами, красивым.

Секрет первый: контроль количества задач, выполняемых параллельно

Во время разработки программного обеспечения бывают случаи, когда многие задачи необходимо выполнять асинхронно, но часто необходимо ограничить количество задач, которые могут выполняться параллельно, чтобы избежать снижения производительности, вызванного одновременным выполнением слишком большого количества асинхронных задач. Например, когда поисковые роботы получают контент из Интернета параллельно, максимальное количество потоков выполнения должно быть ограничено в зависимости от ситуации.

В дни, предшествовавшие async / await, такие механизмы, как семафоры, использовались для связи между потоками для координации выполнения потоков, что требовало от разработчика знания технических деталей многопоточности. С помощью async / await все можно легко сделать.

Например, следующий код считывает слова одно за другим из файла «words.txt», который состоит из слов за строкой. Затем он вызывает удаленный API, чтобы получить подробную информацию о словах, например «фонетические символы, значения китайского языка, примеры предложений». Для ускорения обработки необходимо асинхронное программирование, позволяющее одновременно загружать несколько задач, но ограничивать количество одновременных задач (скажем, пять). Код выглядит следующим образом:

программа класса

{

статическая асинхронная задача Основная (строка [] аргументы)

{

ServiceCollection services = новый ServiceCollection ();

services.AddHttpClient ();

services.AddScoped ‹WordProcessor› ();

используя (var sp = services.BuildServiceProvider ())

{

var wp = sp.GetRequiredService ‹WordProcessor› ();

строка [] words = await File.ReadAllLinesAsync («d: /temp/words.txt»);

Список ‹Task› задач = новый Список ‹Task› ();

foreach (слово var в словах)

{

tasks.Add (wp.ProcessAsync (слово));

если (tasks.Count == 5)

{

// ждем, когда будут готовы пять задач

ждать Task.WhenAll (задачи);

tasks.Clear ();

}

}

// ждем остаток меньше пяти.

ждать Task.WhenAll (задачи);

}

Console.WriteLine («готово!»);

}

}

класс WordProcessor

{

частный IHttpClientFactory httpClientFactory;

общедоступный WordProcessor (IHttpClientFactory httpClientFactory)

{

this.httpClientFactory = httpClientFactory;

}

общедоступная асинхронная задача ProcessAsync (строковое слово)

{

Console.WriteLine (слово);

var httpClient = this.httpClientFactory.CreateClient ();

строка json = await httpClient.GetStringAsync («http://dict.e.opac.vip/dict.php?sw= + Uri.EscapeDataString (слово));

ждать File.WriteAllTextAsync («d: / temp / words /» + word + «.txt», json);

}

}

Основной код следующий:

Список ‹Task› задач = новый Список ‹Task› ();

foreach (слово var в словах)

{

tasks.Add (wp.ProcessAsync (слово));

если (tasks.Count == 5)

{

// ждем, когда будут готовы пять задач

ждать Task.WhenAll (задачи);

tasks.Clear ();

}

}

Возвращенный объект Task не изменяется с помощью «await». Вместо этого он сохраняет возвращенный объект Task в списке. Поскольку мы не ждем с «ожиданием», мы добавляем следующую Задачу в список, не дожидаясь завершения одной Задачи. Когда список заполнен пятью задачами, мы вызываем «await Task.whenAll (tasks)»; дождаться завершения этих пяти задач перед обработкой следующей группы. await Task.WhenAll (tasks) вне цикла используется для обработки последнего набора из менее чем пяти задач.

Секрет второй: как выполнить инъекцию DI в BackgroundService

При внедрении зависимостей (DI) внедренные объекты имеют жизненные циклы. Например, при использовании services.AddDbContext ‹TestDbContext› () для внедрения объекта DbContext в EF Core время жизни TestDbContext равно Scope. TestDbContext может быть введен непосредственно в обычный контроллер MVC, но TestDbContext не может быть введен непосредственно в BackgroundService. Вместо этого вы можете внедрить объект IServiceCopeFactory, затем вызвать метод IServiceCopeFactory.CreateScope () для создания объекта IServiceScope при использовании объекта TestDbContext и использовать IServiceScope.ServiceProvider для ручного получения объекта TestDbContext.

Код выглядит следующим образом:

открытый класс TestBgService: BackgroundService

{

закрытый только для чтения IServiceScopeFactory scopeFactory;

общедоступный TestBgService (IServiceScopeFactory scopeFactory)

{

this.scopeFactory = scopeFactory;

}

защищенная переопределенная задача ExecuteAsync (CancellationToken stoppingToken)

{

используя (var scope = scopeFactory.CreateScope ())

{

var sp = scope.ServiceProvider;

var dbCtx = sp.GetRequiredService ‹TestDbContext› ();

foreach (var b в dbCtx.Books)

{

Console.WriteLine (b.Title);

}

}

return Task.CompletedTask;

}

}

Секрет третий: вызов асинхронных методов без ожидания

Когда я разрабатывал Youzack, веб-сайт для изучения языков, у меня была функция поиска слов. Чтобы улучшить скорость отклика клиента, я сохранил подробную информацию о каждом слове на файловом сервере в виде « файл JSON для каждого слова ». Следовательно, когда клиент запрашивает слово, он сначала обращается к файловому серверу, чтобы узнать, существует ли соответствующий статический файл, и, если он есть, загружает статический файл напрямую. Если слово не существует на файловом сервере, вызовите метод API для запроса. После того, как интерфейс API запросит слово из базы данных, он не только вернет подробную информацию о слове клиенту, но и загрузит подробную информацию о слове на файловый сервер. Это позволит клиенту запросить это слово позже, и он сможет запрашивать непосредственно с файлового сервера.

Таким образом, операция «запрос из базы данных к словам подробной информации и загрузка их на файловый сервер» не имеет смысла для клиента и может снизить скорость отклика интерфейса, поэтому я просто перемещаю операцию «Загрузить файловый сервер» в асинхронный метод, а не ждать его с помощью «await».

Псевдокод выглядит следующим образом:

общедоступная асинхронная задача ‹WordDetail› FindWord (строковое слово)

{

var detail = ждать db.FindWordInDBAsync (слово);

_ = storage.UploadAsync ($ ”{word} .json”, detail.ToJsonString ()); // загрузка без ожидания

деталь возврата;

}

В приведенном выше UploadAsync нет вызова ожидания, поэтому, как только он запрашивается из базы данных, детали возвращаются клиенту, оставляя UploadAsync для выполнения в асинхронном потоке.

Знак «_ =» используется для подавления предупреждений компилятора для асинхронных методов, которые не ожидают.

Секрет четвертый: не используйте Thread.Sleep в асинхронном методе.

При написании кода бывают моменты, когда нам нужно «сделать паузу на некоторое время, прежде чем мы продолжим выполнение следующего кода. «Например, если вы вызываете интерфейс HTTP, но не удается, вы можете подождать две секунды, а затем повторить попытку.

В асинхронном методе используйте Task.Delay () вместо Thread.Sleep (), потому что Thread.Sleep () блокирует основной поток и не может достичь цели «использования асинхронизма для увеличения параллелизма системы».

Следующий код неверен:

public async Task ‹IActionResult› TestSleep ()

{

ожидание System.IO.File.ReadAllTextAsync («d: /temp/words.txt»);

Console.WriteLine («сначала сделано»);

Thread.Sleep (2000);

ожидание System.IO.File.ReadAllTextAsync («d: /temp/words.txt»);

Console.WriteLine («вторая сделана»);

вернуть содержимое («xxxxxx»);

}

Приведенный выше код будет компилироваться и выполняться правильно, но это значительно снизит параллелизм системы. Поэтому используйте Task.Delay () вместо Thread.Sleep ().

Верно следующее:

public async Task ‹IActionResult› TestSleep ()

{

ожидание System.IO.File.ReadAllTextAsync («d: /temp/words.txt»);

Console.WriteLine («сначала сделано»);

await Task.Delay (2000); // !!!

ожидание System.IO.File.ReadAllTextAsync («d: /temp/words.txt»);

Console.WriteLine («вторая сделана»);

вернуть содержимое («xxxxxx»);

}

Секрет пятый: как использовать yield с async?

Ключевое слово yield обеспечивает «конвейерную» обработку данных, позволяя пользователю IEnumerable обрабатывать фрагмент данных в результате создания фрагмента данных.

Однако, поскольку yield и async являются синтаксическими сахарами, компилятор компилирует методы в класс, который использует конечный автомат. В результате встречаются два синтаксических сахара, и компилятор сбивается с толку, поэтому yield нельзя использовать непосредственно в асинхронном методе для возврата данных.

Итак, следующий код неверен:

статический асинхронный IEnumerable ‹int› ReadCC ()

{

foreach (строковая строка в await File.ReadAllLinesAsync («d: /temp/words.txt»))

{

yield return line.Length;

}

}

Итак, используйте IAsyncEnumerable вместо IEnumerable, следующий код верен:

статический асинхронный IAsyncEnumerable ‹int› ReadCC ()

{

foreach (строковая строка в await File.ReadAllLinesAsync («d: /temp/words.txt»))

{

yield return line.Length;

}

}

При вызове метода с IAsyncEnumerable, не используя foreach + await, следующий код неверен:

foreach (int i в ожидании ReadCC ())

{

Console.WriteLine (i);

}

Ключевое слово await необходимо переместить перед foreach, это правильно:

ожидание foreach (int i в ReadCC ())

{

Console.WriteLine (i);

}

Компилятор C # написан Microsoft и не поддерживает «foreach (int i in await ReadCC ())», вероятно, потому, что он совместим с предыдущей спецификацией синтаксиса C #.