- Выбирать подходящий инструмент для работы с вводом-выводом и для вычислений
- Писать
IAsyncEnumerable<T>, читать его черезawait foreachи останавливать работу с помощьюCancellationToken - Распараллеливать вычисления с
Parallelи PLINQ и избегать ловушек блокировки
В прошлом модуле ты научился выполнять работу с ожиданием с помощью async/await и Task.WhenAll. Теперь пора различать две разные задачи. Работа с вводом-выводом (сеть, файлы, базы данных) большую часть времени ждёт — ей нужен async/await. Вычислительная работа (расчёты, обработка изображений) загружает процессор — делить её между ядрами помогают Parallel и PLINQ. В этом уроке ты изучишь и то и другое, а также асинхронные потоки и отмену.
| Вид работы | Пример | Инструмент |
|---|---|---|
| Ввод-вывод: ожидание | HTTP-запрос, файл, SQL | async/await, Task.WhenAll |
| Данные, приходящие частями | постраничный API, большой файл | IAsyncEnumerable<T>, await foreach |
| Процессор: вычисления | простые числа, отчёты, изображения | Parallel.For, AsParallel() |
| Долгая или уже ненужная работа | пользователь нажал «Отмена» | CancellationToken |
Асинхронные потоки: IAsyncEnumerable
Task<List<T>> даёт результат, только когда готов весь список. Если данные приходят частями (постраничный API, большой файл), лучше обрабатывать каждый элемент сразу по прибытии. Асинхронный поток — IAsyncEnumerable<T> — объединяет yield return и await, а потребитель читает его через await foreach. В .NET 10 методы LINQ (Where, Select, ToListAsync) доступны и для асинхронных потоков без дополнительного пакета.
await foreach (int score in LoadScoresAsync())
Console.WriteLine($"Received {score}");
List<int> high = await LoadScoresAsync().Where(s => s >= 80).ToListAsync();
Console.WriteLine($"80 and above: {string.Join(", ", high)}");
async IAsyncEnumerable<int> LoadScoresAsync()
{
int[] fromServer = [95, 78, 88];
foreach (int score in fromServer)
{
await Task.Delay(200); // each item arrives from the network
yield return score;
}
}Received 95 Received 78 Received 88 80 and above: 95, 88
Отмена: CancellationToken
Пользователь закрыл страницу, а сервер всё ещё готовит отчёт? Долгую работу нужно уметь останавливать. CancellationTokenSource создаёт сигнал отмены, а его Token передаётся по цепочке во все асинхронные методы. CancelAfter ставит таймер; методы .NET вроде Task.Delay, HttpClient и EF Core принимают токен и при отмене выбрасывают OperationCanceledException. В собственном вычислительном цикле вызывай token.ThrowIfCancellationRequested().
using var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromMilliseconds(450));
try
{
await DownloadAsync(cts.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("Cancelled: it took too long");
}
async Task DownloadAsync(CancellationToken token)
{
for (int part = 1; part <= 5; part++)
{
await Task.Delay(300, token); // throws once the token is cancelled
Console.WriteLine($"Part {part} downloaded");
}
}Part 1 downloaded Cancelled: it took too long
Для простого ограничения по времени саму задачу менять не нужно: WaitAsync(TimeSpan) (с .NET 6) выбрасывает TimeoutException, если результат не пришёл вовремя. Учти: сама работа продолжается в фоне — чтобы действительно её остановить, нужно передать токен.
Task<string> answer = FetchAsync("exam results", TimeSpan.FromSeconds(2));
try
{
Console.WriteLine(await answer.WaitAsync(TimeSpan.FromMilliseconds(500)));
}
catch (TimeoutException)
{
Console.WriteLine("Timeout: showing yesterday's cached results");
}
async Task<string> FetchAsync(string what, TimeSpan delay)
{
await Task.Delay(delay);
return $"Fresh {what}";
}Timeout: showing yesterday's cached results
Параллельные вычисления: Parallel и PLINQ
Parallel.For и Parallel.ForEach делят итерации цикла между потоками пула, а AsParallel() превращает обычный запрос LINQ в PLINQ — параллельный LINQ. Ниже четыре тяжёлых вычисления — подсчёт простых чисел до разных пределов — идут одновременно. Главное правило: каждый поток либо пишет в свою ячейку (results[i]), либо атомарно меняет общую переменную через Interlocked.
using System.Globalization;
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
int[] limits = [1_000_000, 2_000_000, 3_000_000, 4_000_000];
int[] results = new int[limits.Length];
Parallel.For(0, limits.Length, i => results[i] = CountPrimes(limits[i])); // each task writes its own cell
for (int i = 0; i < limits.Length; i++)
Console.WriteLine($"Primes below {limits[i]:N0}: {results[i]:N0}");
long total = 0;
Parallel.ForEach(Enumerable.Range(1, 1000), n => Interlocked.Add(ref total, n));
Console.WriteLine($"Sum 1..1000 = {total}");
int evenSquares = Enumerable.Range(1, 1000).AsParallel().Count(n => n * n % 2 == 0);
Console.WriteLine($"PLINQ, even squares: {evenSquares}");
static int CountPrimes(int limit)
{
int count = 0;
for (int n = 2; n < limit; n++)
{
bool prime = true;
for (int d = 2; d * d <= n; d++)
if (n % d == 0) { prime = false; break; }
if (prime) count++;
}
return count;
}Primes below 1,000,000: 78,498 Primes below 2,000,000: 148,933 Primes below 3,000,000: 216,816 Primes below 4,000,000: 283,146 Sum 1..1000 = 500500 PLINQ, even squares: 500
long total = 0;
Parallel.ForEach(Enumerable.Range(1, 1000), n =>
{
total += n; // read + add + write from many threads: a race
});
// the result can be less than 500500long total = 0;
Parallel.ForEach(Enumerable.Range(1, 1000), n =>
{
Interlocked.Add(ref total, n); // one atomic operation
});
// always 500500 (or simply: Enumerable.Range(1, 1000).AsParallel().Sum())А как вычислительная работа сочетается с асинхронным кодом? В графическом интерфейсе отправь тяжёлый расчёт в поток пула через await Task.Run(() => CountPrimes(4_000_000)): окно не зависает, а результат придёт, когда будет готов. На сервере (ASP.NET Core) Task.Run обычно ничего не даёт — работа просто переходит из одного потока пула в другой. Task.Run нужен, чтобы увести одну работу в фон, а Parallel — чтобы разбить одну большую работу на части.
ConfigureAwait и ловушки блокировки
В приложениях с графическим интерфейсом (WPF, WinForms, MAUI) есть контекст синхронизации: после await код возвращается в UI-поток, чтобы безопасно менять кнопки и текст. Библиотечный код с интерфейсом не работает, поэтому там пишут ConfigureAwait(false) — «продолжение можно выполнить в любом потоке». Это немного быстрее и предотвращает некоторые взаимоблокировки (deadlock). В ASP.NET Core и консольных приложениях такого контекста нет, поэтому в коде приложения ConfigureAwait обычно не нужен.
// library code: it does not need to come back to the caller's UI thread
public static async Task<int> CountLinesAsync(string path, CancellationToken token = default)
{
string[] lines = await File.ReadAllLinesAsync(path, token).ConfigureAwait(false);
return lines.Count(line => line.Length > 0);
}ConfigureAwait(false)// blocks the thread while waiting;
// in a UI app (WPF, WinForms) this can freeze or deadlock
string report = LoadReportAsync().Result;// the thread is free while waiting
string report = await LoadReportAsync();Главное
- Для ввода-вывода —
async/await, для вычислений —Parallelи PLINQ. IAsyncEnumerable<T>+await foreachобрабатывают элементы по мере поступления; в .NET 10 для них есть LINQ.CancellationTokenпередаётся по цепочке; при отмене выбрасываетсяOperationCanceledException, а для лимита времени естьWaitAsync.- В параллельном коде каждый поток пишет в свою ячейку или использует
Interlocked. - «Async до конца»: никаких
.Resultи.Wait(); в библиотечном коде —ConfigureAwait(false).
Проверь себя
Вопросов: 10. Каждый правильный ответ приносит XP.