数百名消费者和大文件的好方法

时间:2014-05-30 15:14:25

标签: c# .net multithreading task-parallel-library tpl-dataflow

我有几个文件(每个近1GB)和数据。数据是一个字符串行。

我需要与数百名消费者一起处理这些文件。这些消费者中的每一个都做一些与其他消费者不同的处消费者不会同时写任何地方。他们只需要输入字符串。处理后,他们更新本地缓冲区。消费者可以轻松地并行执行。

重要提示:对于一个特定文件,每个消费者必须以正确的顺序处理所有行(不跳过)(因为它们出现在文件中)。处理不同文件的顺序无关紧要。

一个消费者对单行的处理速度相当快。我希望Corei5上的时间不到50微秒。

所以现在我正在寻找解决这个问题的好方法。 这将成为.NET项目的一部分,所以请让我们坚持使用.NET(最好是C#)。

我了解TPL和DataFlow。我想最相关的是BroadcastBlock。但我认为这里的问题是,每一行我都要等待所有消费者完成才能发布新的消费者。我想这样效率不高。

我认为理想的情况是这样的:

  1. 一个线程从文件读取并写入缓冲区。
  2. 每个消费者在准备就绪时,同时从缓冲区读取行并处理它。
  3. 当一个消费者阅读时,不应删除缓冲区中的条目。只有在所有消费者都处理完毕后才能将其删除。
  4. TPL自行安排消费者线程。
  5. 如果一个消费者的表现优于其他消费者,则不应该等待,并且可以从缓冲区中读取更多近期条目。
  6. 我采用这种方法是对的吗?无论是否,我如何实施良好的解决方案?

2 个答案:

答案 0 :(得分:1)

我不同意一个线程从文件读取并写入缓冲区
在几个1 GB的文件中,该线程会占用太多内存 .NET具有对象大小限制,集合是一个对象

你需要节流阅读线
我想你可以用BlockingCollection做到这一点 公元前100万处理的是保持最慢的消费者忙碌 它还为打开下一个文件提供了一些缓冲区

using System.IO;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;

namespace BlockingCollection2
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }
        public static void BC_GetConsumingEnumerableCollection()
        {
            List<string> fileNames = new List<string>();  // add filesNames
            string producerLine;
            System.IO.StreamReader file;
            List<BCtaskBC> bcs = new List<BCtaskBC>();  // add for each consumer
            // Kick off a producer task
            Task.Factory.StartNew(() =>
            {
                foreach(string fileName in fileNames)
                {
                    file = new System.IO.StreamReader(fileName);
                    while ((producerLine = file.ReadLine()) != null)
                    {
                        foreach (BCtaskBC bc in bcs)
                        {
                            // string is reference type but it often acts like a value type
                            // may need to make a deep copy of producerLine for this next line
                            bc.BC.Add(producerLine);  // if  any queue size gets to 1000000 then this blocks
                        }
                    }
                    file.Close();
                }                 
                // Need to do this to keep foreach below from hanging
                foreach (BCtaskBC bc in bcs)
                {
                    bc.BC.CompleteAdding();
                }
            });

            // Now consume the blocking collection with foreach. 
            // Use bc.GetConsumingEnumerable() instead of just bc because the 
            // former will block waiting for completion and the latter will 
            // simply take a snapshot of the current state of the underlying collection. 
            //  Method signature: Parallel.ForEach(IEnumerable<TSource> source, Action<TSource> body)
            Parallel.ForEach(bcs, bc =>
            {
                foreach (string consumerLine in bc.BC.GetConsumingEnumerable())
                {
                    bc.BCtask.ProcessTask(consumerLine);  
                }
            } //close lambda expression
                 ); //close method invocation 
            // I think this need to be parallel
            //foreach (BCtaskBC bc in bcs)
            //{
            //    foreach (string consumerLine in bc.BC.GetConsumingEnumerable())
            //    {
            //        bc.BCtask.ProcessTask(consumerLine);
            //    }
            //}
        }
        public abstract class BCtaskBC
        {   // may need to do something to make this thread safe   
            private BlockingCollection<string> bc = new BlockingCollection<string>(1000000);  // this trotttles the size
            public BCtask BCtask { get; set; }
            public BlockingCollection<string> BC { get { return bc; } }
        }
        public abstract class BCtask
        {   // may need to do something to make this thread safe 
            public void ProcessTask(string S) {}
        }
    }
}

答案 1 :(得分:0)

我最近解决了类似的问题。但是我的解决方案不在C#中,因为我有很高的耐用性要求,所以它在SQL中。但也许我的一些想法可以帮助你(这就是我要做的):

我使用了“工作单元”范例。在您的情况下,您可以选择一个工作单位,例如100-1000行文字。在您的情况下,每个工作单元可以通过文件名,起始文件位置和结束文件位置来表征。每个单元还有一个标志,告诉它是否由特定消费者处理。我的工作单元存储为DB记录;您可以将它们保存为简单的内存结构中的对象,例如list。

应用程序启动后,将启动一个单独的线程,按顺序读取所有文件,并将工作单元添加到List。该线程有一个要处理的文件列表,它按顺序读取特定数量的行,注释文件位置,并将文件名和文件位置保存到列表中。

只要列表中有一些工作单元可供处理,消费者就会从列表的开头开始处理单位。要获取特定单元的特定文本行,消费者使用缓存对象。只要所有消费者从列表的开头开始处理,所有消费者都很有可能要求相同的缓存工作单元,至少在开始时。

缓存对象完全独立于将工作单元添加到列表的线程。此对象的确切实现取决于一些其他要求,例如,如果其中一个消费者崩溃或挂起,或者如果应用程序重新启动该怎么办,或者您是否同意“快速”消费者等待“慢”消费者,你想如何监控整个过程等......

希望这会有所帮助......