2014-09-01 115 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);