2014-09-01 110 views
2

我有一个很奇怪的问题。我的WebClient.DownloadDataCompleted大多数时间不会启动。WebClient.DownloadDataCompleted not firing

我使用这个类:

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孤立呼吁DownloadFilesAsyncDownloadDataCompleted之后半秒或所谓的,符合市场预期。

但是,在我的真实应用程序中,它根本不会触发,等待它完成的代码就会卡住。如评论所示,我有两个断点。他们没有被击中。
啊,但有时它确实会起火。相同的URL,相同的代码,只是一个新的调试会话。根本没有模式。

我检查可用线程的线程池:workerThreads> 30K,completionPortThreads = 999

我加10秒的休眠恢复之前,我的Web客户端都没有垃圾睡眠后检查收集和我的事件处理程序仍然附加。

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

+0

我会说,上面的代码中,'clients'会超出范围并可能受到GC的打击。但是你指出,当你在这个方法中睡觉时,“客户”显然还在。如果你添加睡眠,下载是否工作?或者你仍然有同样的行为?你还需要多久才能下载所有的下载文件? – 2014-09-01 14:22:19

+0

当你在没有附加调试器的情况下在VS中启动项目时,你会得到相同的行为吗? – 2014-09-01 14:23:24

+0

@steve:添加睡眠不会改变行为,下载仍然不起作用。下载时间不到一秒钟,URL是用于localhost的,因此连接问题也不存在。由于传递给'cancellationToken.Register'的操作,'clients'不会超出范围。 – 2014-09-01 14:24:12

回答

1

从评论:

某处后,有一个Task.WaitAll它等待这个和其他任务。但是,(1)我不明白为什么这会影响异步下载 - 请详细说明 - (2)问题没有消失,当我添加睡眠时,Task.WaitAll将不会被调用

看来你有一个死锁造成的Task.WaitAll。我可以throughly here解释:

当你await返回一个TaskTask<T>异步方法,有一个由TaskAwaitableTask.GetAwaiter方法产生的SynchronizationContext的隐式捕获。

一旦同步上下文就位和异步方法调用完成时,TaskAwaitable尝试编组的延续(这基本上是该方法的其余部分与第一await关键字之后调用)到SynchronizationContext(使用SynchronizationContext.Post),将其先前被捕获。如果调用的线程是被阻止,等待同样的方法完成,你有一个死锁

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

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

+1

OP注释即使'WebClient.DownloadDataCompleted'不会触发,它将在线程池中运行,并且不会涉及'await'。 – 2014-09-01 14:55:57

+0

@ErenErsönmez准确地说,我没有使用WebClient中返回任务的方法! – 2014-09-01 14:59:12

+0

但是您正在等待'从'tcs'返回的任务# – 2014-09-01 16:16:29

1

根据该意见,不是一个理想的答案但你可以在foreach之前和之后临时改变同步方面:

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

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

SynchronizationContext.SetSynchronizationContext(syncContext);