WebClient.DownloadDataCompleted未触发

时间:2014-09-01 14:09:54

标签: c# multithreading webclient webclient-download

我有一个非常奇怪的问题。我的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&gt; 30k,completionPortThreads = 999。

我在返回前添加了10秒钟的睡眠,并在睡眠后检查我的网络客户端是否已被垃圾收集并且我的事件处理程序仍然附加。

现在,我没有想法来解决这个问题 还有什么可能导致这种奇怪的行为?

2 个答案:

答案 0 :(得分:1)

来自评论:

  

稍后,有一个Task.WaitAll等待这个和其他任务。但是,(1)我不明白为什么这会影响异步下载 - 请详细说明 - 以及(2)当我添加睡眠时问题不会消失,因此,不会调用Task.WaitAll < / p>

您似乎有Task.WaitAll引起的死锁。我可以通过here解释它:

当您await返回TaskTask<T>的异步方法时,生成的SynchronizationContext会隐式捕获TaskAwaitable通过Task.GetAwaiter方法。

一旦该同步上下文到位并且异步方法调用完成,TaskAwaitable会尝试将延续(基本上是第一个await关键字之后的其余方法调用)编组到先前捕获的SynchronizationContext(使用SynchronizationContext.Post)。如果调用线程阻止,等待相同的方法完成,则会出现死锁

当您致电Task.WaitAll时,您将阻止所有任务完成,这将使编组回到原始上下文不可能,并且基本上是死锁。

不使用Task.WaitAll,而是使用await Task.WhenAll

答案 1 :(得分:1)

根据评论,不是理想的答案,但您可以在foreach之前和之后临时更改同步上下文:

var syncContext = SynchronizationContext.Current;
SynchronizationContext.SetSynchronizationContext(null);

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

SynchronizationContext.SetSynchronizationContext(syncContext);