快速有效地下载多个文件(异步)

时间:2014-05-27 14:08:08

标签: c# .net multithreading async-await

我有很多文件需要下载。所以我尝试使用新的异步功能,如下所示。

var streamTasks = urls.Select(async url => (await WebRequest.CreateHttp(url).GetResponseAsync()).GetResponseStream()).ToList();

var streams = await Task.WhenAll(streamTasks);
foreach (var stream in streams)
{
    using (var fileStream = new FileStream("blabla", FileMode.Create))
    {
        await stream.CopyToAsync(fileStream);
    }
}

我担心这段代码会导致大量内存使用,因为如果有1000个文件包含2MB文件,那么这段代码会将1000 * 2MB流加载到内存中吗?

我可能会遗漏某些东西,或者我完全正确。如果我没有错过任何东西,那么最好等待每个请求和消费流是最好的方法吗?

3 个答案:

答案 0 :(得分:5)

这两种选择都可能存在问题。一次只下载一个并不会缩放并且需要花费时间,而一次下载所有文件可能会造成太大的负担(同样,在处理它们之前无需等待所有文件下载)。

我更喜欢使用可配置的大小限制此类操作。一个简单的方法是使用AsyncLock(利用SemaphoreSlim)。更健壮的方法是将TPL DataflowMaxDegreeOfParallelism一起使用。

var block = new ActionBlock<string>(url =>
    {
        var stream = (await WebRequest.CreateHttp(url).GetResponseAsync()).GetResponseStream();
        using (var fileStream = new FileStream("blabla", FileMode.Create))
        {
            await stream.CopyToAsync(fileStream);
        }
    },
    new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 100 });

答案 1 :(得分:3)

无论您是否使用async,您的代码都会将流加载到内存中。执行async工作会通过返回调用方来处理I / O部分,直到ResponseStream返回。

您必须选择async,而不是关于阅读大流输入的程序的实施。

如果我是你,我会考虑如何将工作量分成块。您可以并行读取ResponseStream并将每个流保存到不同的源(可能是文件)并从内存中释放。

答案 2 :(得分:2)

这是我自己的答案,来自Yuval Itzchakov的chunking想法,我提供实施。请为此实施提供反馈。

foreach (var chunk in urls.Batch(5))
{
    var streamTasks = chunk
        .Select(async url => await WebRequest.CreateHttp(url).GetResponseAsync())
        .Select(async response => (await response).GetResponseStream());

    var streams = await Task.WhenAll(streamTasks);

    foreach (var stream in streams)
    {
        using (var fileStream = new FileStream("blabla", FileMode.Create))
        {
            await stream.CopyToAsync(fileStream);
        }
    }
}

批量是扩展方法,如下所示。

public static IEnumerable<IEnumerable<T>> Batch<T>(this IEnumerable<T> source, int chunksize)
{
    while (source.Any())
    {
        yield return source.Take(chunksize);
        source = source.Skip(chunksize);
    }
}