【问题标题】:AWAIT multiple file downloads with DownloadDataAsync使用 DownloadDataAsync 等待多个文件下载
【发布时间】:2014-08-18 20:45:49
【问题描述】:

我有一个 zip 文件创建器,它接收一个 String[] 的 Urls,并返回一个包含 String[] 中所有文件的 zip 文件

我想会有很多这样的例子,但我似乎找不到“如何异步下载许多文件并在完成后返回”的答案

如何一次下载 {n} 个文件,并仅在所有下载完成后返回字典?

private static Dictionary<string, byte[]> ReturnedFileData(IEnumerable<string> urlList)
{
    var returnList = new Dictionary<string, byte[]>();
    using (var client = new WebClient())
    {
        foreach (var url in urlList)
        {
            client.DownloadDataCompleted += (sender1, e1) => returnList.Add(GetFileNameFromUrlString(url), e1.Result);
            client.DownloadDataAsync(new Uri(url));
        }
    }
    return returnList;
}

private static string GetFileNameFromUrlString(string url)
{
    var uri = new Uri(url);
    return System.IO.Path.GetFileName(uri.LocalPath);
}

【问题讨论】:

    标签: c# .net async-await webclient


    【解决方案1】:
    • 首先,您使用async-await 标记了您的问题,但并未实际使用它。真的没有理由再使用旧的异步范式了。
    • 异步等待所有并发的async 操作完成,您应该使用Task.WhenAll,这意味着您需要在实际提取结果之前将所有任务保存在某个构造(即字典)中.
    • 最后,当您拥有所有结果时,您只需将uri 解析为文件名,然后从async 任务中提取结果来创建新的结果字典。

    async Task<Dictionary<string, byte[]>> ReturnFileData(IEnumerable<string> urls)
    {
        var dictionary = urls.ToDictionary(
            url => new Uri(url),
            url => new WebClient().DownloadDataTaskAsync(url));
    
        await Task.WhenAll(dictionary.Values);
    
        return dictionary.ToDictionary(
            pair => Path.GetFileName(pair.Key.LocalPath),
            pair => pair.Value.Result);
    }
    

    【讨论】:

    • 我将此标记为正确,但是当我将其合并到代码中时,我遇到了线程错误。我用此示例替换了 ASYNC 代码,并且可以正确返回 Byte[],但我无法使用您的模式投射对象:stackoverflow.com/questions/8874477/…
    • @Wesley 该示例为每个请求创建一个新的WebClient,而您没有。 WebClient 不是线程安全的(显然不支持并发异步调用)。
    • 感谢您尝试@|3arnon。它似乎不仅仅是WebClient 创建,因为该方法仍在尝试在完成之前返回。在另一个示例中,我们正在创建一个任务集合并在对返回的结果运行 LINQ 调用之前直接在 Task 集合上调用 WhenAll。除非您有其他方法,否则我可能只需要处理一下然后再回来。
    • @Wesley 这里也发生了同样的事情(只有任务在 Dictionary 而不是 IEnumerable)。该方法在完成之前不会返回,因为 Task.WhenAll 等待所有任务结束,如果没有,Task.Result 就会有。
    • HttpClient.GetByteArrayAsync 会是更好的选择,因为 HttpClient 是线程安全的,您可以对所有调用使用单个实例,并且您只有一个实例可以处理
    【解决方案2】:
        public string JUST_return_dataURL_by_URL(string URL, int interval, int max_interval)
        {
            var client = new WebClient(proxy);
            client.Headers = _headers;
            string downloaded_from_URL = "false";       //default - until downloading
            client.DownloadDataCompleted += bytes => 
            {
                Console.WriteLine("Done!");
                string dataURL = Convert.ToBase64String( bytes );
                string filename = Guid.NewGuid().ToString().Trim('{', '}')+".png";
                downloaded_from_URL =
                            "Image Downloaded from " + URL
                        +   "<br>"
                        +   "<a href=\""+dataURL+"\" download=\""+filename+"\">"
                        +       "<img src=\"data:image/png;base64," + dataURL + "\"/>"+filename
                        +   "</a>"
                ;
                return;
            };
            client.DownloadDataAsync(new System.Uri(URL));
    
            int i = 0;
            do{
            //  Console.WriteLine(
            //      "(interval > 10): "+(interval > 10)
            //      +"\n(downloaded_from_URL == \"false\"): " + (downloaded_from_URL == "false")
            //      +"\ninterval: "+interval
            //  );
                Thread.Sleep(interval);
                i+=interval;
            }
            while( (downloaded_from_URL == "false") && (i < max_interval) );
    
            return downloaded_from_URL;
        }
    

    【讨论】:

      【解决方案3】:

      你会想要 task.WaitAll 方法...

      msdn link

      将每个下载创建为单独的任务,然后将它们作为集合传递。

      一个捷径可能是将你的下载方法包装在一个任务中。

      Return new Task<downloadresult>(()=>{ method body});
      

      为含糊而道歉,在 iPad 上工作对编码来说很糟糕。

      编辑:

      另一个值得考虑的实现是使用并行框架包装下载。

      由于您的任务都使用参数执行相同的操作,因此您可以改用 Parallel.Foreach 并将其包装到单个任务中:

      public System.Threading.Tasks.Task<System.Collections.Generic.IDictionary<string, byte[]>> DownloadTask(System.Collections.Generic.IEnumerable<string> urlList)
              {
                  return new System.Threading.Tasks.Task<System.Collections.Generic.IDictionary<string, byte[]>>(() =>
                  {
                      var r = new System.Collections.Concurrent.ConcurrentDictionary<string, byte[]>();
                      System.Threading.Tasks.Parallel.ForEach<string>(urlList, (url, s, l) =>
                      {
                          using (System.Net.WebClient client = new System.Net.WebClient())
                          {
                              var bytedata = client.DownloadData(url);
                              r.TryAdd(url, bytedata);
                          }
                      });
      
      
                      var results = new System.Collections.Generic.Dictionary<string, byte[]>();
                      foreach (var value in r)
                      {
                          results.Add(value.Key, value.Value);
                      }
      
                      return results;
                  });
              }
      

      这利用并发集合来支持方法内的并行访问,然后再转换回 IDictionary。

      此方法返回一个任务,因此可以通过等待调用。

      希望这提供了一个有用的替代方案。

      【讨论】:

      • 看来我需要将DownloadDataAsync 替换为DownloadDataTaskAsync。这个答案对我说:“去学习 MSFT 异步堆栈,当你了解更多时再回来。”我可以举一个更详细的例子吗?
      • @Wesley 你是对的;这不是一个合适的答案。当然,你的问题差不多就是“我懒得去学习如何写一个异步程序,所以你给我写吧,这样我就不需要学习如何去做了。”这不合适。
      • 替代方案不是异步的,因此可扩展性较差。
      • 它返回一种可以等待的Task,使其异步。然后它在内部以并行线程处理输入集合。
      • @kidshaw 实际的 IO 操作 (DownloadData) 是同步的,因此您在整个操作期间都持有这些线程。这会损害可扩展性。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2014-12-28
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-11-22
      • 1970-01-01
      相关资源
      最近更新 更多