【问题标题】:Parallel Task Library WaitAny Design并行任务库 WaitAny Design
【发布时间】:2013-05-14 02:22:06
【问题描述】:

我刚刚开始探索 PTL 并且有一个设计问题。

我的场景: 我有一个 URL 列表,每个 URL 都引用一个图像。我希望并行下载每个图像。下载至少一个图像后,我想执行一个对下载的图像执行某些操作的方法。该方法不应该是并行的——它应该是串行的。

我认为以下方法可行,但我不确定这是否是正确的方法。因为我有单独的类来收集图像和对收集到的图像做“某事”,所以我最终传递了一个似乎错误的任务数组,因为它暴露了如何检索图像的内部工作原理。但我不知道有什么办法。实际上,这两种方法都有更多内容,但这并不重要。只要知道它们真的不应该被归为一种既能检索图像又能对图像进行处理的大型方法。

//From the Director class
Task<Image>[] downloadTasks = collector.RetrieveImages(listOfURLs);

for (int i = 0; i < listOfURLs.Count; i++)
{
    //Wait for any of the remaining downloads to complete
    int completedIndex = Task<Image>.WaitAny(downloadTasks);
    Image completedImage = downloadTasks[completedIndex].Result;

    //Now do something with the image (this "something" must happen serially)
    //Uses the "Formatter" class to accomplish this let's say
}

///////////////////////////////////////////////////

//From the Collector class
public Task<Image>[] RetrieveImages(List<string> urls)
{
    Task<Image>[] tasks = new Task<Image>[urls.Count];

    int index = 0;
    foreach (string url in urls)
    {
        string lambdaVar = url;  //Required... Bleh
        tasks[index] = Task<Image>.Factory.StartNew(() =>
            {
                using (WebClient client = new WebClient())
                {
                    //TODO: Replace with live image locations
                    string fileName = String.Format("{0}.png", i);
                    client.DownloadFile(lambdaVar, Path.Combine(Application.StartupPath, fileName));
                }

                return Image.FromFile(Path.Combine(Application.StartupPath, fileName));
            },
            TaskCreationOptions.LongRunning | TaskCreationOptions.AttachedToParent);

        index++;
    }

    return tasks;
}

【问题讨论】:

    标签: c# task-parallel-library


    【解决方案1】:

    当您不关心任何其他任务的结果时,通常会使用 WaitAny 等待一项任务。例如,如果您只关心碰巧返回的第一张图片。

    这个怎么样。

    这将创建两个任务,一个加载图像并将它们添加到阻塞集合中。第二个任务等待集合并处理添加到队列中的任何图像。当所有图像都加载完毕后,第一个任务关闭队列,这样第二个任务就可以关闭了。

    using System;
    using System.Collections.Concurrent;
    using System.Collections.Generic;
    using System.Drawing;
    using System.IO;
    using System.Net;
    using System.Threading.Tasks;
    
    namespace ClassLibrary1
    {
        public class Class1
        {
            readonly string _path = Directory.GetCurrentDirectory();
    
            public void Demo()
            {
                IList<string> listOfUrls = new List<string>();
                listOfUrls.Add("http://i3.codeplex.com/Images/v16821/editicon.gif");
                listOfUrls.Add("http://i3.codeplex.com/Images/v16821/favorite-star-on.gif");
                listOfUrls.Add("http://i3.codeplex.com/Images/v16821/arrow_dsc_green.gif");
                listOfUrls.Add("http://i3.codeplex.com/Images/v16821/editicon.gif");
                listOfUrls.Add("http://i3.codeplex.com/Images/v16821/favorite-star-on.gif");
                listOfUrls.Add("http://i3.codeplex.com/Images/v16821/arrow_dsc_green.gif");
                listOfUrls.Add("http://i3.codeplex.com/Images/v16821/editicon.gif");
                listOfUrls.Add("http://i3.codeplex.com/Images/v16821/favorite-star-on.gif");
                listOfUrls.Add("http://i3.codeplex.com/Images/v16821/arrow_dsc_green.gif");
    
                BlockingCollection<Image> images = new BlockingCollection<Image>();
    
                Parallel.Invoke(
                    () =>                   // Task 1: load the images
                    {
                        Parallel.For(0, listOfUrls.Count, (i) =>
                            {
                                Image img = RetrieveImages(listOfUrls[i], i);
                                img.Tag = i;
                                images.Add(img);    // Add each image to the queue
                            });
                        images.CompleteAdding();    // Done with images.
                    },
                    () =>                   // Task 2: Process images serially
                    {
                        foreach (var img in images.GetConsumingEnumerable())
                        {
                            string newPath = Path.Combine(_path, String.Format("{0}_rot.png", img.Tag));
                            Console.WriteLine("Rotating image {0}", img.Tag);
                            img.RotateFlip(RotateFlipType.RotateNoneFlipXY);
    
                            img.Save(newPath);
                        }
                    });
            }
    
            public Image RetrieveImages(string url, int i)
            {
                using (WebClient client = new WebClient())
                {
                    string fileName = Path.Combine(_path, String.Format("{0}.png", i));
                    Console.WriteLine("Downloading {0}...", url);
                    client.DownloadFile(url, Path.Combine(_path, fileName));
                    Console.WriteLine("Saving {0} as {1}.", url, fileName);
                    return Image.FromFile(Path.Combine(_path, fileName));
                }
            } 
        }
    }
    

    警告:代码没有任何错误检查或取消。已经很晚了,你需要做点什么吗? :)

    这是管道模式的一个示例。它假设获取图像非常慢,并且锁定在阻塞集合中的成本不会导致问题,因为与下载图像所花费的时间相比,它发生的频率相对较低。

    我们的书...您可以在http://parallelpatterns.codeplex.com/ 阅读有关此模式和其他并行编程模式的更多信息 第 7 章介绍了管道,随附的示例展示了具有错误处理和取消功能的管道。

    【讨论】:

      【解决方案2】:

      TPL 已经提供了 ContinueWith 函数来在另一个任务完成时执行另一个任务。任务链是 TPL 中用于异步操作的主要模式之一。

      以下方法下载一组图像并通过重命名每个文件来继续

      static void DownloadInParallel(string[] urls)
      {
         var tempFolder = Path.GetTempPath();
      
         var downloads = from url in urls
                         select Task.Factory.StartNew<string>(() =>{
                             using (var client = new WebClient())
                             {
                                 var uri = new Uri(url);
                                 string file = Path.Combine(tempFolder,uri.Segments.Last());
                                 client.DownloadFile(uri, file);
                                 return file;
                             }
                         },TaskCreationOptions.LongRunning|TaskCreationOptions.AttachedToParent)
                        .ContinueWith(t=>{
                             var filePath = t.Result;
                             File.Move(filePath, filePath + ".test");
                        },TaskContinuationOptions.ExecuteSynchronously);
      
          var results = downloads.ToArray();
          Task.WaitAll(results);
      }
      

      您还应该检查 ParallelExtensionsExtras 示例中的 WebClient Async Tasks。 DownloadXXXTask 扩展方法处理任务的创建和文件的异步下载。

      以下方法使用 DownloadDataTask 扩展来获取图像的数据并在将其保存到磁盘之前对其进行旋转

      static void DownloadInParallel2(string[] urls)
      {
          var tempFolder = Path.GetTempPath();
      
          var downloads = from url in urls
               let uri=new Uri(url)
               let filePath=Path.Combine(tempFolder,uri.Segments.Last())
               select new WebClient().DownloadDataTask(uri)                                                        
               .ContinueWith(t=>{
                  var img = Image.FromStream(new MemoryStream(t.Result));
                  img.RotateFlip(RotateFlipType.RotateNoneFlipY);
                  img.Save(filePath);
               },TaskContinuationOptions.ExecuteSynchronously);
      
          var results = downloads.ToArray();
          Task.WaitAll(results);
      }
      

      【讨论】:

      • 两件事,我不认为 TaskContinuationOptions.ExecuteSynchronously 做我需要的。 “某事”(在您的示例中移动文件)不能在多个线程上同时发生。让我们假设它不是移动文件而是通过串行电缆与设备通信。第二,就像我说的那样,它比我简化的要多。我认为将这两个任务合并到同一个方法中是不合适的。但这迫使我绕过任务,这似乎是一种糟糕的模式。
      • 所谓的坏模式是 TPL 的实际设计理念。在这件事上它实际上非常接近 F#。其次,ExecuteSynchronously 意味着延续将使用与它之前的任务相同的线程运行。最后,您没有将任务组合到同一个方法中。传递给任务或延续的 lambda 是另一个匿名函数。您可以轻松地传递方法名称而不是使用 lambda。如果您发现 TPL 的工作方式让您不舒服,您可能应该寻找不同的库或模式,而不是尝试与之对抗。
      • 这很可能就是答案。如果是这种情况,您将获得支持并被接受。至于我的两点,我认为我们的沟通出现了问题。 1) ExecuteSynchronously 意味着延续可能同时在几个线程上运行。在我的情况下,在任何给定时间,延续只能在一个线程上运行。 2)我知道它在技术上是它自己的方法,我也可以加入一个方法调用。但是这两件事实际上是在两个完全独立的类中,它们甚至不应该相互了解。因此我的困境。
      【解决方案3】:

      最好的方法可能是实现观察者模式:让你的RetreiveImages 函数实现IObservable,将你的“完成的图像动作”放入IObserver 对象的OnNext 方法中,然后订阅它到RetreiveImages

      我自己还没有尝试过(仍然需要更多地使用任务库),但我认为这是“正确”的做法。

      【讨论】:

      • 我认为这可能是因为我试图同时学习 PTL 和观察者模式,但我似乎无法正确理解。如果我有 Collector 类(如图所示)、Director 类(顶部代码 sn-p)和 Formatter 类(执行“某事”的东西),我将如何实现它?
      【解决方案4】:

      //下载所有图片

      private async void GetAllImages ()
      {
          var downloadTasks = listOfURLs.Where(url =>   !string.IsNullOrEmpty(url)).Select(async url =>
                  {
                      var ret = await RetrieveImage(url);
                      return ret;
              }).ToArray();
      
              var counts = await Task.WhenAll(downloadTasks);
      }
      
      //From the Collector class
      public async Task<Image> RetrieveImage(string url)
      {
          var lambdaVar = url;  //Required... Bleh
          using (WebClient client = new WebClient())
          {
              //TODO: Replace with live image locations
              var fileName = String.Format("{0}.png", i);
              await client.DownloadFile(lambdaVar, Path.Combine(Application.StartupPath, fileName));
          }
          return Image.FromFile(Path.Combine(Application.StartupPath, fileName));
      }  
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2015-01-14
        • 1970-01-01
        • 1970-01-01
        • 2011-10-25
        相关资源
        最近更新 更多