WebClient.DownloadDataCompleted事件未触发。

4

我有一个非常奇怪的问题。我的WebClient.DownloadDataCompleted大部分时间都不会触发。

我正在使用这个类:

public class ParallelFilesDownloader
{
    public Task DownloadFilesAsync(IEnumerable<Tuple<Uri, Stream>> files, CancellationToken cancellationToken)
    {
        var localFiles = files.ToArray();
        var tcs = new TaskCompletionSource<object>();
        var clients = new List<WebClient>();

        cancellationToken.Register(
            () =>
            {
                // Break point here
                foreach (var wc in clients.Where(x => x != null))
                    wc.CancelAsync();
            });

        var syncRoot = new object();
        var count = 0;
        foreach (var file in localFiles)
        {
            var client = new WebClient();

            client.DownloadDataCompleted += (s, args) =>
            {
                // Break point here
                if (args.Cancelled)
                    tcs.TrySetCanceled();
                else if (args.Error != null)
                    tcs.TrySetException(args.Error);
                else
                {
                    var stream = (Stream)args.UserState;
                    stream.Write(args.Result, 0, args.Result.Length);
                    lock (syncRoot)
                    {
                        count++;
                        if (count == localFiles.Length)
                            tcs.TrySetResult(null);
                    }
                }
            };
            clients.Add(client);

            client.DownloadDataAsync(file.Item1, file.Item2);
        }

        return tcs.Task;
    }
}

当我在LINQPad中独立调用DownloadFilesAsync时,预期情况下大约半秒钟后就会调用DownloadDataCompleted
然而,在我的实际应用程序中,它根本不会触发,等待它完成的代码被卡住了。如注释所示,我有两个断点,但都没有被击中。但是,有时它确实会触发。相同的URL、相同的代码,只是一个新的调试会话,没有任何规律可言。
我检查了线程池中可用的线程:workerThreads > 30k,completionPortThreads = 999。
在返回之前,我添加了10秒的休眠,并在休眠后检查我的Web客户端没有被垃圾回收,我的事件处理程序仍然附加着。
现在,我已经没有更多的想法来解决这个问题了。还有什么可能导致这种奇怪的行为?

希望你不要调用 DownloadFilesAsync(......).Wait() - L.B
@L.B 在稍后的某个位置,有一个Task.WaitAll等待此任务和其他任务。但是,(1)我不明白这会对异步下载产生什么影响,请详细说明;(2)当我添加睡眠时问题并没有消失,因此将不会调用Task.WaitAll - Daniel Hilgarth
@DanielHilgarth 尝试使用 await (await Task.WhenAll) 调用它,而不会阻塞调用线程(我怀疑会出现死锁)。 - L.B
你尝试过将这个问题转化为一个小的完整实例吗?你的代码中还有其他怪异的东西吗?就像你所说的,如果代码在其他环境中没有问题,那可能是其他地方出了问题... - steve cook
1
如果DownloadDataCompleted在调用线程的同步上下文中运行,则会出现相同的问题。 - L.B
显示剩余16条评论
2个回答

1
根据评论,这不是一个理想的答案,但是在foreach之前和之后可以暂时更改同步上下文:
var syncContext = SynchronizationContext.Current;
SynchronizationContext.SetSynchronizationContext(null);

foreach (var file in localFiles)
{
    ...
}

SynchronizationContext.SetSynchronizationContext(syncContext);

1

从评论中可以看到:

稍后的某个地方,有一个等待它和其他任务的Task.WaitAll。 然而,(1)我不明白为什么这会影响异步下载 - 请解释一下 - (2)当我添加了sleep并且Task.WaitAll将不会被调用时,问题并没有消失

看起来你遇到了由Task.WaitAll引起的死锁。我可以通过这里来详细解释:

当你await一个返回TaskTask<T>的异步方法时,Task.GetAwaiter方法生成的TaskAwaitable会隐式捕获SynchronizationContext

一旦同步上下文到位并且异步方法调用完成,TaskAwaitable 将尝试将续延 (也就是第一个 await 关键字后面的所有方法调用) 通过之前捕获的 SynchronizationContext (使用 SynchronizationContext.Post) 调度。如果调用线程被阻塞,等待同一方法完成,则会发生死锁
当您调用 Task.WaitAll 时,您会阻塞,直到所有任务都完成,这将使返回到原始上下文无法实现,从而导致死锁。
请改用 await Task.WhenAll,而非使用 Task.WaitAll

1
OP指出,即使是在线程池上运行且没有涉及await的WebClient.DownloadDataCompleted事件也不会触发。 - Eren Ersönmez
@ErenErsönmez 没错。我没有使用返回任务的WebClient方法! - Daniel Hilgarth
但是你正在等待从tcs返回的Task - Yuval Itzchakov
@DanielHilgarth 我知道你没有使用返回Task的重载。但是你的OnCompleted事件可能在同一个同步上下文中触发,从而导致死锁。你说你正在使用.NET 4.0,我建议你安装Microsoft.Bcl.Async以获取async-await功能。 - Yuval Itzchakov

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接