Ожидание завершения цикла асинхронных вызовов c#
Есть цикл, который асинхронно скачивает много файлов:
privare void MyLogic(...) {
int start = 5, end = 150;
string filepath = @"somepath", url=@"someurl", ext = ".zip";
for (int i = start; i<= end; i++)
{
LoadFile(filepath + @"\" + i.ToString("D4"), url + i.ToString() + ext);
}
//вот тут я хочу выполнить обработку только после того, как все загрузки будут завершены
}
private void LoadFile(string url, string path)
{
try
{
using (WebClient client = new WebClient())
{
client.DownloadFileCompleted += (s, e) =>
{
Console.WriteLine(url + " success");
};
client.DownloadFileAsync(new Uri(url), path); //асинхронная загрузка
}
}
catch
{
Console.WriteLine(url + " error"); //досадно, но продолжаем
}
}
После цикла я хочу обработать на уровне файловой системы то, что скачалось. Как мне переписать код, чтобы точно понять в какой момент успешная загрузка всех файлов завершилась?
Ответы (2 шт):
И так для начала, вам надо изменить LoadFile сделав его Task, а не void. Например, так
private async Task LoadFileAsync(string url, string path)
{
try
{
using (WebClient client = new WebClient())
{
client.DownloadFileCompleted += (s, e) =>
{
Console.WriteLine(url + " success");
};
await client.DownloadFileAsync(new Uri(url), path); //асинхронная загрузка
}
}
catch
{
Console.WriteLine(url + " error"); //досадно, но продолжаем
}
}
А вот с MyLogic есть три варианта:
1) Выполняем все загрузки по одной по очереди, но асинхронно
private async Task MyLogic(...)
{
...
for (int i = start; i<= end; i++)
{
await LoadFileAsync(filepath + @"\" + i.ToString("D4"), url + i.ToString() + ext);
}
DoAfter();
}
2) Запускаем все 100500 загрузок сразу и ждём их окончания
private async Task MyLogic(...)
{
...
var taks = new List<Task>();
for (int i = start; i<= end; i++)
{
Task task = LoadFileAsync(filepath + @"\" + i.ToString("D4"), url + i.ToString() + ext);
tasks.Add(task);
}
await Task.WhenAll(tasks);
DoAfter();
}
3) Всё таки запускать неизвестное какое количество задач одновременно не очень прилично. И для этого придуман паттерн Throttling (ссылка на википедию). Конечно, его можно реализовать самому, а можно воспользоваться библиотекой Task Parallel Libraray.Dataflow (ссылка на нюгет)
private async Task MyLogic(...)
{
...
var block = new ActionBlock<int>(
i => LoadFileAsync(filepath + @"\" + i.ToString("D4"), url + i.ToString() + ext),
new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 50 });
for (int i = start; i<= end; i++)
{
block.Post(i);
}
block.Complete();
await block.Completion;
DoAfter();
}
Вот пример с полной асинхронностью кода и использованием System.Net.Http.HttpClient.
Консольное приложение, .NET Core 3.1 (C# 8.0)
Полный код приложения с комментариями
class Program
{
// HttpClient создается один раз на все время работы приложения
private static readonly HttpClient client = new HttpClient();
struct DownloadParams // структура для параметров загрузки
{
public string Url { get; set; }
public string FilePath { get; set; }
}
// асинхронный Main, обратите внимание!
static async Task Main(string[] args)
{
List<DownloadParams> downloads = new List<DownloadParams>(); // новый список загрузок
int start = 5, end = 150;
string filepath = @"somepath", url = @"someurl", ext = ".zip";
for (int i = start; i <= end; i++) // заполнение списка загрузок
{
downloads.Add(new DownloadParams { Url = url + i.ToString() + ext, FilePath = filepath + "/" + i.ToString("D4") });
}
try
{
await DownloadFilesAsync(downloads); // загружаем
}
catch (Exception ex)
{
Console.WriteLine("Ошибка при загрузке: " + ex.Message);
}
// здесь код, который надо выполнить после завершения всех загрузок.
// при этом, все файлы уже записаны на диск, можно открывать и обрабатывать
}
// асинхронно загружает весь список с ограничением на количество одновременных загрузок
private static async Task DownloadFilesAsync(List<DownloadParams> downloads)
{
Console.WriteLine("Запуск загрузки.");
int maxTasks = 20; // максимальное количество одновременно работающих задач
using SemaphoreSlim semaphore = new SemaphoreSlim(maxTasks);
List<Task> tasks = new List<Task>();
foreach (DownloadParams download in downloads)
{
// ждать, если достигнуто максимальное количество одновременно работающих задач, пока их не станет меньше
await semaphore.WaitAsync();
tasks.Add(Task.Run(async () =>
{
await DownloadAndSaveFileAsync(download); // загрузить файл
semaphore.Release(); // сообщить, что задач стало меньше
}));
}
await Task.WhenAll(tasks); // ждать завершения всех задач
Console.WriteLine("Загрузка завершена.");
}
// асинхронно загружает один файл
private static async Task DownloadAndSaveFileAsync(DownloadParams download)
{
Console.WriteLine("Начата загрузка из: " + download.Url + ", и сохранение в: " + download.FilePath);
try
{
using HttpResponseMessage responseMessage = await client.GetAsync(download.Url, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
responseMessage.EnsureSuccessStatusCode(); // бросить исключение, если сервер вернул ошибку
using FileStream fileStream = new FileStream(download.FilePath, FileMode.Create, FileAccess.Write, FileShare.None); // новый файл
using Stream stream = await responseMessage.Content.ReadAsStreamAsync().ConfigureAwait(false); // загрузка данных
await stream.CopyToAsync(fileStream); // запись на диск
Console.WriteLine("Файл " + download.FilePath + " успешно загружен и сохранен.");
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
}
Если с асинхронным Main сложно, то вот обычный синхронный
static void Main(string[] args)
{
List<DownloadParams> downloads = new List<DownloadParams>(); // новый список загрузок
int start = 5, end = 150;
string filepath = @"somepath", url = @"someurl", ext = ".zip";
for (int i = start; i <= end; i++) // заполнение списка загрузок
{
downloads.Add(new DownloadParams { Url = url + i.ToString() + ext, FilePath = filepath + "/" + i.ToString("D4") });
}
try
{
DownloadFilesAsync(downloads).GetAwaiter().RetGesult(); // загружаем
}
catch (Exception ex)
{
Console.WriteLine("Ошибка при загрузке: " + ex.Message);
}
}