Ответ
Для параллельного выполнения независимых задач в C# есть несколько основных подходов. Выбор зависит от необходимости ожидания результатов и обработки исключений.
1. Task.WhenAll — ожидание завершения всех задач
Используется, когда нужно запустить задачи параллельно и дождаться результатов всех из них.
public async Task ProcessDataInParallelAsync()
{
var task1 = DownloadFileAsync("url1");
var task2 = ProcessImageAsync("image.jpg");
var task3 = QueryDatabaseAsync();
// Все задачи выполняются параллельно. Ожидаем завершения всех.
await Task.WhenAll(task1, task2, task3);
// Получаем результаты (если задачи возвращают значение).
var file = await task1; // await здесь мгновенный, т.к. задача уже завершена.
var result = task2.Result; // Альтернатива для уже завершенной задачи.
}
2. Task.WhenAny — ожидание первой завершенной задачи
Полезно для реализации таймаутов или выбора самого быстрого источника данных.
public async Task<string> GetFastestResponseAsync()
{
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var tasks = new List<Task<string>>
{
CallServiceAAsync(cts.Token),
CallServiceBAsync(cts.Token)
};
// Ожидаем, пока любая из задач завершится успешно.
Task<string> completedTask = await Task.WhenAny(tasks);
// Отменяем оставшиеся задачи.
cts.Cancel();
// Возвращаем результат первой завершенной задачи.
return await completedTask;
}
3. Parallel.For / Parallel.ForEach (для CPU-bound операций)
Используется для параллельной обработки данных в коллекциях, когда операции требуют интенсивных вычислений.
public void TransformDataInParallel(List<DataItem> items)
{
var parallelOptions = new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount };
Parallel.ForEach(items, parallelOptions, item =>
{
// Вычислительно сложная операция над каждым элементом.
item.Value = HeavyComputation(item.RawData);
});
}
4. Параллельный запуск с ограничением степени параллелизма (SemaphoreSlim)
Когда нужно выполнить много I/O-bound задач (например, HTTP-запросов), но с ограничением на их одновременное количество.
public async Task ProcessUrlsWithThrottlingAsync(List<string> urls, int maxConcurrent)
{
var semaphore = new SemaphoreSlim(maxConcurrent);
var tasks = urls.Select(async url =>
{
await semaphore.WaitAsync();
try
{
return await DownloadStringAsync(url);
}
finally
{
semaphore.Release();
}
});
await Task.WhenAll(tasks);
}
Ключевой момент: Task.WhenAll и Task.WhenAny не запускают задачи — они лишь создают задачи-ожидания. Задачи начинают выполняться в момент их создания (если только они не созданы с опцией TaskCreationOptions.RunContinuationsAsynchronously). Для CPU-bound операций предпочтительнее Parallel или Parallel LINQ (PLINQ).
Ответ 18+ 🔞
А, ну ты про параллельность в C# спрашиваешь? Ну это, блядь, классика жанра, каждый второй джуниор на этом спотыкается, как пьяный на льду. Сейчас разжуем, как бабушка котлету.
Смотри, есть несколько основных способов, как заставить твой код работать не по-пионерски, в одну строчку, а сразу всем скопом. Выбор зависит от того, что тебе надо: просто запустить и забыть, или дождаться всех, как мамкины котлеты на сковородке.
1. Task.WhenAll — ждём всех, как последнего идиота
Используется, когда тебе надо запустить кучу асинхронных дел и дождаться, пока все нихуя не закончат. Типа, скачать три файла, обработать картинку и запросить базу — и всё это одновременно, а не по очереди, как в совковой очереди за колбасой.
public async Task ProcessDataInParallelAsync()
{
// Запускаем всё сразу. Они уже начинают работать, блядь!
var task1 = DownloadFileAsync("url1");
var task2 = ProcessImageAsync("image.jpg");
var task3 = QueryDatabaseAsync();
// Сидишь тут и ждёшь, пока все три задачи не приползут на финиш.
// Пока ждёшь, поток не блокируется, можешь чай пить.
await Task.WhenAll(task1, task2, task3);
// Всё, задачи упёрлись лбом в стенку и закончили. Теперь можно результаты забирать.
var file = await task1; // await тут мгновенный, потому что задача уже в анабиозе.
var result = task2.Result; // Или так, если ты любишь жить опасно и без await.
}
2. Task.WhenAny — кто первый встал, того и тапки
Это когда тебе нужен результат от самого шустрого ублюдка. Например, дернул два апишки за погодой — и какая служба быстрее ответит, ту и считаешь правдой. Остальные посылаешь нахуй, чтобы зря ресурсы не жрали.
public async Task<string> GetFastestResponseAsync()
{
// Ставим таймер, чтобы через 5 секунд всем стало похуй на наши запросы.
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var tasks = new List<Task<string>>
{
CallServiceAAsync(cts.Token),
CallServiceBAsync(cts.Token)
};
// Сидим, пялимся в экран. Как только хоть одна задача моргнёт — хватаем её.
Task<string> completedTask = await Task.WhenAny(tasks);
// Всем остальным трусливым зайцам кричим "отмена!".
cts.Cancel();
// А победителю достаётся наша благодарность в виде возврата его результата.
return await completedTask;
}
3. Parallel.For / ForEach — для тяжёлых вычислений, где мозги плавятся
Это когда у тебя не ввод-вывод, а именно CPU-bound задача — типа перемножить матрицы размером с твою квартиру. Тут асинхронность не катит, нужна именно параллельность по ядрам.
public void TransformDataInParallel(List<DataItem> items)
{
// Говорим: "Работай, но больше, чем ядер у процессора, потоков не создавай, а то комп взвоет".
var parallelOptions = new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount };
Parallel.ForEach(items, parallelOptions, item =>
{
// Каждому элементу коллекции вмазываем по полной программе.
// Каждый обрабатывается в своём потоке, если ядра позволяют.
item.Value = HeavyComputation(item.RawData); // Тут твой процессор будет потеть.
});
}
4. SemaphoreSlim — чтоб не наебашить тысячу запросов сразу и не сломать сервер
Классика для I/O-bound задач, типа скачивания по сотне урлов. Запустить все сразу — сервер тебя заблокирует или сам сдохнет. Надо делать с умом, с ограничением.
public async Task ProcessUrlsWithThrottlingAsync(List<string> urls, int maxConcurrent)
{
// Создаём семафор — как турникет в метро. Одновременно пропустит только maxConcurrent котов.
var semaphore = new SemaphoreSlim(maxConcurrent);
// Для каждого урла создаём задачу.
var tasks = urls.Select(async url =>
{
// Подходим к турникету и ждём, пока он пропустит.
await semaphore.WaitAsync();
try
{
// Проскочили — делаем своё грязное дело.
return await DownloadStringAsync(url);
}
finally
{
// Выходим — освобождаем место для следующего.
semaphore.Release();
}
});
// Ждём, пока все урлы не пройдут через эту мясорубку.
await Task.WhenAll(tasks);
}
Важный момент на засыпку: Task.WhenAll и WhenAny — это не стартёры, блядь. Они не запускают задачи. Задачи стартуют в момент их создания, если только ты специально не сделал их ленивыми, как студент на паре. А эти методы — просто такие наблюдатели, которые сидят и ждут, пока кто-то из бегунов прибежит или все сразу. Для CPU-bound операций, где надо грузить процессор, юзай Parallel или PLINQ, а не асинхронные таски — это разные вещи, как молоток и микроскоп.