任务并行库 - 任务工厂行为

时间:2015-08-05 15:03:58

标签: c# multithreading asynchronous task-parallel-library large-files

我有大(> 1 Gb)的文本文件。我需要以多线程的方式逐行处理该文件(应用业务逻辑),所以我编写了下一个代码:

public Task Parse(Stream content, Action<Trade> parseCallback)
{    
   return Task.Factory.StartNew(() =>
   {
      using (var streamReader = new StreamReader(content))
      {
        string line;
        while ((line = streamReader.ReadLine()) != null)
        {
            if (String.IsNullOrWhiteSpace(line))
            {
                continue;
            }

            var tokens = line.Split(TokensSeparator);
            if (!tokens.Any() || tokens.Count() != 6)
            {
                continue;
            }

            Task.Factory.StartNew(() => parseCallback(new Trade
            {
                Id = Int32.Parse(tokens[0]),
                MktPrice = Decimal.Parse(tokens[1], CultureInfo.InvariantCulture),
                Notional = Decimal.Parse(tokens[2], CultureInfo.InvariantCulture),
                Quantity = Int64.Parse(tokens[3]),
                TradeDate = DateTime.Parse(tokens[4], CultureInfo.InvariantCulture),
                TradeType = tokens[5]
            }),
            TaskCreationOptions.AttachedToParent);
        }
      }
   });
}

其中 Action parseCallback 对从数据行创建的数据对象应用业务逻辑。

Parse()方法返回Task,调用者线程等待父任务完成:

try
{
   var parseTask = parser.Parse(fileStream, AddTradeToTradeResult);
   parseTask.Wait();
}
catch (AggregateException ae)
{
   throw new ApplicationException(ae.Flatten().InnerException.Message, ae);
}

问题是:

  1. 很明显, while 循环中的任务可以比处理更快地创建。 TPL将如何处理这些排队的任务?他们会等到线程池中的某个线程选择并执行它们还是有可能丢失它们?
  2. 调用者线程( parseTask.Wait())是主控制台应用程序线程。在大型文件处理期间,我是否能够与控制台应用程序窗口进行交互,否则它将被阻止?
  3. 我意识到提供的方法是错误的。我该如何改进解决方案?例如:读取文件流并将数据放入主线程中的某个队列,在任务的帮助下处理队列项。还有其他方法吗?请指点我。

3 个答案:

答案 0 :(得分:0)

2 + 3:如果启动第二个线程,并让它创建任务,则UI不会被阻止。 但是,您不必这样做 - 您的主线程可以创建任务列表,并等待它们全部(Task.WhenAll)。正如您所说,在循环中创建任务非常快,并且UI将仅在创建任务所花费的时间内被阻止。

编辑:

我只是意识到你根本不使用异步,这使我的回答无关紧要。为什么不使用异步从磁盘读取?您可以异步读取磁盘中的大量数据(这是您必须耗费时间的一部分程序,不是吗?)并在它们到达时对它们进行处理。

EDIT2:

这听起来像是一个典型的制片人 - 消费者场景(我希望我是对的)。请查看以下示例:您有一个线程(主线程,认为它不一定是)从文件中异步读取行,并将它们推送到队列中。另一个线程,即消费者,在到达并处理它们时拾取线条。我没有测试代码,我认为它不能很好地工作,这只是一个开始的例子。希望它有所帮助。

    class ProducerConsumer
    {
        private BlockingCollection<string> collection;
        ICollection<Thread> consumers;
        string fileName;
        public ProducerConsumer(string fileName)
        {
            this.fileName =  fileName;
            collection = new BlockingCollection<string>();
            consumers = new List<Thread>();
            var consumer = new Thread(() => Consumer());
            consumers.Add(consumer);
            consumer.Start();
        }
        private async Task StartWork()
        {
            using (TextReader reader = File.OpenText(fileName))
            {
                var line = await reader.ReadLineAsync();  
                collection.Add(line);
            }
        }
        private void Consumer()
        {
            while (true /* insert your abort condition here*/)
            {
                try
                {
                    var line = collection.Take();
                    // Do whatever you need with this line. If proccsing this line takes longer then 
                    // fetching the next line (that is - the queue lenght increasing too fast) - you
                    // can always launch an additional consumer thread.
                }
                catch (InvalidOperationException) { }
            }
        }
    }

您可以启动专用线程 - 而不是主线程 - 作为生产者。因此,它将读取文件并以尽可能快的速度将项目添加到队列中,并且您的磁盘可以。如果这对您的消费者来说太快了 - 只需再推出一个!

答案 1 :(得分:0)

您可以通过应用信号量来控制线程 如果需要,它将运行最多320个线程,然后等待完成早期的线程。

 public class Utitlity
    {
        public static SemaphoreSlim semaphore = new SemaphoreSlim(300, 320);
        public static char[] TokensSeparator = "|,".ToCharArray();
        public async Task Parse(Stream content, Action<Trade> parseCallback)
        {
            await Task.Run(async () =>
            {
                using (var streamReader = new StreamReader(content))
                {
                    string line;
                    while ((line = streamReader.ReadLine()) != null)
                    {
                        if (String.IsNullOrWhiteSpace(line))
                        {
                            continue;
                        }

                        var tokens = line.Split(TokensSeparator);
                        if (!tokens.Any() || tokens.Count() != 6)
                        {
                            continue;
                        }
                        await semaphore.WaitAsync();
                        await Task.Run(() =>
                        {
                            var trade = new Trade
                        {
                            Id = Int32.Parse(tokens[0]),
                            MktPrice = Decimal.Parse(tokens[1], CultureInfo.InvariantCulture),
                            Notional = Decimal.Parse(tokens[2], CultureInfo.InvariantCulture),
                            Quantity = Int64.Parse(tokens[3]),
                            TradeDate = DateTime.Parse(tokens[4], CultureInfo.InvariantCulture),
                            TradeType = tokens[5]
                        };
                            parseCallback(trade);

                        });
                        semaphore.Release();
                    }
                }
            });
        }
    }

    public class Trade
    {
        public int Id { get; set; }
        public decimal MktPrice { get; set; }
        public decimal Notional { get; set; }
        public long Quantity { get; set; }
        public DateTime TradeDate { get; set; }
        public string TradeType { get; set; }


    }

答案 2 :(得分:0)

更改Parse以便它返回一行懒惰的IEnumerable<string>行。实际上,您可以使用内置File.EnumerateLines来删除大多数代码。

然后,使用PLINQ查询:

File.EnumerateLines(path)
.AsParallel()
.Where(x => !String.IsNullOrWhiteSpace(line))
.Select(line => ProcessLine(line);

就是这样。这将以更加平和的程度运行。确切的DOP由TPL选择。该算法很不稳定,您可能需要添加WithDegreeOfParallelism(ProcessorCount)

如果您愿意,可以通过附加parseCallback来致电.ForAll(parseCallback)

  

在大文件处理期间我是否可以与控制台应用程序窗口进行交互,否则它将被阻止?

为了做到这一点,你需要在Task.Run中包装这个PLINQ查询,以便它在后台线程上执行。