【发布时间】:2022-03-02 18:20:37
【问题描述】:
在编写一个类以从服务器并行下载图像时(使用由DataFlow TPL 库支持的消费者/生产者模式),使用ActionBlock 和Flurl.Http 设施方法DownloadFileAsync,我已经意识到取消需要很长时间。由于所有下载操作都共享一个CancellationToken,我希望所有任务都会立即(或几乎)取消。实际上,如果我产生大量并行下载,取消所有任务/线程可能需要几分钟。我通过将ExecutionDataflowBlockOptions.MaxDegreeOfParallelism 属性设置为10 来解决这个问题。这样一来,在任何给定时间最多有10 个并发下载要取消(这仍然不是我期望的立即行动)。
我创建了一个控制台 .NET 5 程序,它可以单独重现问题(没有 DataFlow TPL、ActionBlock 等)。它首先询问并发下载的数量(默认按 Enter 键:即 100 次下载)。然后它使用Flurl.Http(使用HttpClient)并行生成所有这些下载,并将CancellationToken 传递给每个操作。然后它等待按键被按下,然后通过调用CancellationTokenSource.Cancel 方法取消挂起的下载。最后,它会打印一些统计信息,包括成功和失败/取消的下载次数,以及完成取消所用的时间。完整代码如下:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Flurl.Http;
const string imageSource = "https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png";
const int defaultCount = 100;
var watch = Stopwatch.StartNew();
int completed = 0;
int failed = 0;
Console.WriteLine("Flurl.DownloadFileAsync Cancellation test!");
Console.Write($"Number of downloads ({defaultCount}): ");
var input = Console.ReadLine();
if (!int.TryParse(input, out var count))
count = defaultCount;
Console.WriteLine($"Will spawn {count} parallel downloads of '{imageSource}'");
CancellationTokenSource cts = new CancellationTokenSource();
List<Task> tasks = new();
for (int i = 0; i < count; i++)
tasks.Add(Download(i));
Console.WriteLine("Hit anything to cancel...");
Console.ReadKey(true);
log("Cancelling pending downloads");
var cancelMoment = watch.Elapsed;
cts.Cancel();
Task.WaitAll(tasks.ToArray());
log("Downloads cancelled. Program ended!");
Console.WriteLine($"### Total cancellation time: {watch.Elapsed - cancelMoment} -> Completed: {completed}, Failed/Cancelled: {failed}");
async Task Download(int i) {
var fn = $"test_{i}.png";
try {
await imageSource.DownloadFileAsync(fn, cancellationToken: cts.Token);
Interlocked.Increment(ref completed);
log($"DONE: {fn}");
} catch(Exception e) {
Interlocked.Increment(ref failed);
log($"# ERROR: {fn}/r/n >> {e.Message}");
}
}
void log(string s) => Console.WriteLine($"{watch.Elapsed}- {s}");
最让我印象深刻的是,允许所有下载完成(即使我输入 1000 次下载)比取消操作要快。我不知道是否发生了任何类型的死锁(这会导致操作在锁定超时后结束),或者这些下载的取消是否只是被破坏了。对于这个问题,我找不到很好的解释或解决方案。
要重现该问题,您必须在所有下载完成之前按一个键取消挂起的下载。如果你的时间正确,你可以让一些下载成功。如果你打得太快,你将取消所有下载。如果您等待的时间过长,所有下载都已经完成。
这次运行产生了以下结果:
取消 99 个待处理的操作花费了 55 多秒的时间。如果我只是让所有下载完成,它所花费的时间比取消相同操作所花费的时间要少很多。
更新
我已经完全删除了 Flurl 并直接使用 HttpClient 下载文件,问题仍然存在。我已将 Downlod 方法更改为以下内容:
async Task Download(int i) {
var fn = $"test_{i}.png";
try {
var r = await client.GetAsync(imageSource, cancellationToken: cts.Token);
using var httpStm = await r.Content.ReadAsStreamAsync(cts.Token);
var file = new FileInfo(fn);
using var f = file.OpenWrite();
await httpStm.CopyToAsync(f, cts.Token);
Interlocked.Increment(ref completed);
log($"DONE: {fn}");
} catch(Exception e) {
Interlocked.Increment(ref failed);
log($"# ERROR: {fn}/r/n >> {e.Message}");
}
}
结果与基于 FLURL 的实现相同(毕竟,Flurl.Http 只是 HttpClient 的包装器)。
更新 2
我已将下载方法更改为简单地等待可取消的Task.Delay,现在取消 100 次操作的时间约为 2 秒。虽然它更快,但它不是瞬时的,并且根据屏幕上日志的时间,在我看来,取消是按顺序触发的,而不是并行/一次触发的。本次下载代码为:
async Task Download(int i) {
var fn = $"test_{i}.png";
try {
await Task.Delay(TimeSpan.FromMinutes(1), cts.Token);
Interlocked.Increment(ref completed);
log($"DONE: {fn}");
} catch (Exception e) {
Interlocked.Increment(ref failed);
log($"# ERROR: {fn}/r/n >> {e.Message}");
}
}
以下屏幕截图显示了上面代码的结果:
有人对此有很好的解释或解决方案吗?
【问题讨论】:
-
我猜在下载返回之前它实际上并没有检查令牌。也许您应该在调用
DownloadAsync之前检查令牌 -
您能否检查是否将取消令牌添加到执行中,例如:
Task.WaitAll(tasks.ToArray(), cts.Token);是您想要的结果吗? -
在不登录控制台的情况下试一试,不要使用互锁增量。仅在任务启动时才尝试打印到控制台,然后在所有任务都停止时打印出来。看看最小化的输出是否会有所作为。
-
@quain 已经这样做了! :-) 看来 HttpClient 是罪魁祸首......它只是不能很好地“取消”......不过仍在研究。
标签: c# task-parallel-library dotnet-httpclient cancellationtokensource flurl