快速有效地下载多个文件(异步)
我有这么多文件,我必须下载。 所以我尝试使用新的异步function,如下所示。
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流加载到内存中?
我可能会遗漏一些东西,或者我完全正确。 如果我没有错过任何东西,那么最好等待每个请求和消费流是最好的方法吗?
这两种选择都可能存在问题。 一次只下载一个不会扩展并且需要花费时间,同时一次下载所有文件可能是一个负载过多(也就是说,在处理它们之前无需等待所有文件下载)。
我更喜欢用可配置的大小限制这种操作。 一种简单的方法是使用AsyncLock
(它使用SemaphoreSlim
)。 更健壮的方法是使用具有MaxDegreeOfParallelism
TPL Dataflow
。
var block = new ActionBlock(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 });
无论您是否使用async
您的代码都会将流加载到内存中。 执行async
工作通过返回调用方来处理I / O部分,直到ResponseStream
返回为止。
您必须选择让asnt关注async
,而是执行有关读取大流输入的程序。
如果我是你,我会考虑如何将工作量分成块。 您可以并行读取ResponseStream
并将每个流保存到不同的源(可能是文件)并从内存中释放。
这是我自己的答案,来自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> Batch(this IEnumerable source, int chunksize) { while (source.Any()) { yield return source.Take(chunksize); source = source.Skip(chunksize); } }