在定时循环中处理多个线程(一个一个)

时间:2016-01-28 09:52:52

标签: c# multithreading timer batch-processing

我需要在一夜之间处理大量文件,并定义开始和结束时间以避免中断用户。我一直在调查,但现在处理线程的方法太多了,我不确定要走哪条路。这些文件作为附件进入Exchange收件箱。

我目前的尝试,基于这里的一些例子和一些实验,是:

 while (DateTime.Now < dtEndTime.Value)
 {
            var finished = new CountdownEvent(1);
            for (int i = 0; i < numThreads; i++)
            {


                object state = offset;

                finished.AddCount();
                ThreadPool.QueueUserWorkItem(delegate
                {
                    try
                    {
                        StartProcessing(state);
                    }
                    finally
                    {
                        finished.Signal();
                    }
                });

                offset += numberOfFilesPerPoll;

            }
            finished.Signal();
            finished.Wait(); 


        }

此刻它在winforms应用程序中运行是为了方便,但核心处理是在一个DLL中,所以我可以从一个Windows服务,从一个在调度程序下运行的控制台生成我需要的类,但是最容易的。我确实设置了一个带有Timer对象的Windows服务,该对象在配置文件中设置的时间启动处理。

所以我的问题是 - 在上面的代码中,我初始化了一堆线程(目前是10个),然后等待它们全部处理。我的理想是一个静态的线程数,当一个完成时我触发另一个,然后当我到达结束时间我只是等待所有线程完成。 这样做的原因是我处理的文件是可变大小的 - 有些可能需要几秒钟才能处理,有些可能需要几个小时,所以我不希望整个应用程序在一个线程完成时等待,如果可以的话让它在后台滴答作响。 (编辑)就目前而言,每个线程都实例化一个类并向其传递一个偏移量。然后,该类从收件箱中获取下一个x电子邮件,从偏移量开始(使用Exchange Web服务分页功能)。在处理每个文件时,它会移动到单独的文件夹中。从目前为止的一些回复中,我想知道我是否应该抓住外部循环中的电子邮件,并根据需要生成线程。 为了解决这个问题,我目前有一些积压的电子邮件,我试图通过这些电子邮件处理。一旦积压清单,夜间运行的负载可能会显着降低。

平均每晚要处理大约1000个文件。

更新

我已经重写了大量的代码,以便我可以使用Parallel.Foreach并且我遇到了线程安全问题。调用代码现在看起来像这样:

public bool StartProcessing()
        {

            FindItemsResults<Item> emails = GetEmails();



            var source = new CancellationTokenSource(TimeSpan.FromHours(10));

            // Process files in parallel, with a maximum thread count.
            var opts = new ParallelOptions { MaxDegreeOfParallelism = 8, CancellationToken = source.Token };

            try
            {
                Parallel.ForEach(emails, opts, processAttachment);
            }

            catch (OperationCanceledException)
            {
                Console.WriteLine("Loop was cancelled.");
            }
            catch (Exception err)
            {
                WriteToLogFile(err.Message + "\r\n");
                WriteToLogFile(err.StackTrace + "r\n");
            }
            return true;
        }

到目前为止一切顺利(借助临时错误处理)。我现在有一个新问题,因为&#34;项目&#34;对象,这是一封电子邮件,不是线程安全的。例如,当我开始处理电子邮件时,我将其移至&#34;处理&#34;文件夹,以便其他进程无法抓住它 - 但事实证明,有几个线程可能正在尝试一次处理同一封电子邮件。我如何保证这不会发生?我知道我需要添加一个锁,我可以在ForEach中添加它还是应该在processAttachments方法中?

4 个答案:

答案 0 :(得分:2)

使用TPL:

Parallel.ForEach( EnumerateFiles(),
                  new ParallelOptions { MaxDegreeOfParallelism = 10 },
                  file => ProcessFile( file ) );

EnumerateFiles停止枚举您的结束时间,如下所示:

IEnumerable<string> EnumerateFiles()
{
    foreach (var file in Directory.EnumerateFiles( "*.txt" ))
        if (DateTime.Now < _endTime)
            yield return file;
        else
            yield break;
}

答案 1 :(得分:1)

您可以使用Parallel.ForEach()和取消令牌来源的组合,这将在一段时间后取消操作:

using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace Demo
{
    static class Program
    {
        static Random rng = new Random();

        static void Main()
        {
            // Simulate having a list of files.
            var fileList = Enumerable.Range(1, 100000).Select(i => i.ToString());

            // For demo purposes, cancel after a few seconds.
            var source = new CancellationTokenSource(TimeSpan.FromSeconds(10));

            // Process files in parallel, with a maximum thread count.
            var opts = new ParallelOptions {MaxDegreeOfParallelism = 8, CancellationToken = source .Token};

            try
            {
                Parallel.ForEach(fileList, opts, processFile);
            }

            catch (OperationCanceledException)
            {
                Console.WriteLine("Loop was cancelled.");
            }
        }

        static void processFile(string file)
        {
            Console.WriteLine("Processing file: " + file);

            // Simulate taking a varying amount of time per file.

            int delay;

            lock (rng)
            {
                delay = rng.Next(200, 2000);
            }

            Thread.Sleep(delay);

            Console.WriteLine("Processed file: " + file);
        }
    }
}

作为使用取消令牌的替代方法,您可以编写一个返回IEnumerable<string>的方法,该方法返回文件名列表,并在时间结束时停止返回它们,例如:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace Demo
{
    static class Program
    {
        static Random rng = new Random();

        static void Main()
        {
            // Process files in parallel, with a maximum thread count.
            var opts = new ParallelOptions {MaxDegreeOfParallelism = 8};
            Parallel.ForEach(fileList(), opts, processFile);
        }

        static IEnumerable<string> fileList()
        {
            // Simulate having a list of files.
            var fileList = Enumerable.Range(1, 100000).Select(x => x.ToString()).ToArray();

            // Simulate finishing after a few seconds.
            DateTime endTime = DateTime.Now + TimeSpan.FromSeconds(10);

            int i = 0;

            while (DateTime.Now <= endTime)
                yield return fileList[i++];
        }

        static void processFile(string file)
        {
            Console.WriteLine("Processing file: " + file);

            // Simulate taking a varying amount of time per file.

            int delay;

            lock (rng)
            {
                delay = rng.Next(200, 2000);
            }

            Thread.Sleep(delay);

            Console.WriteLine("Processed file: " + file);
        }
    }
}

请注意,您不需要使用此方法进行try / catch。

答案 2 :(得分:0)

您应该考虑使用Microsoft的Reactive Framework。它允许您使用LINQ查询以非常简单的方式处理多线程异步处理。

这样的事情:

var query =
    from file in filesToProcess.ToObservable()
    where DateTime.Now < stopTime
    from result in Observable.Start(() => StartProcessing(file))
    select new { file, result };

var subscription =
    query.Subscribe(x =>
    {
        /* handle result */
    });

确实,如果StartProcessing已经定义,那就是您需要的所有代码。

Just NuGet&#34; Rx-Main&#34;。

哦,要随时停止处理,只需致电subscription.Dispose()

答案 3 :(得分:0)

这是一项真正令人着迷的任务,我花了一段时间才把代码提升到一个令我满意的程度。

我最终得到了上述的组合。

首先要注意的是,我将以下行添加到我的Web服务调用中,因为我遇到的操作超时,我认为是因为我超过了端点设置的某些限制,实际上是由于微软在.Net 2.0中设置的限制:

ServicePointManager.DefaultConnectionLimit = int.MaxValue;
ServicePointManager.Expect100Continue = false;

有关详细信息,请参阅此处:

What to set ServicePointManager.DefaultConnectionLimit to

只要我添加了这些代码行,我的处理速度就会从10分钟增加到100分钟左右。

但是我仍然对循环和分区等不满意。我的服务转移到物理服务器上以最小化CPU争用,我想让操作系统决定它的运行速度,而不是我的代码限制它

经过一些研究,这就是我最终得到的 - 可能不是我编写的最优雅的代码,但它非常快速和可靠。

List<XElement> elements = new List<XElement>();
 while (XMLDoc.ReadToFollowing("ElementName"))
     {
   using (XmlReader r = XMLDoc.ReadSubtree())
      {
   r.Read();
   XElement node = XElement.Load(r);
//do some processing of the node here...
elements.Add(node);
}
}
//And now pass the list of elements through PLinQ to the actual web service call, allowing the OS/framework to handle the parallelism

int failCount=0; //the method call below sets this per request; we log and continue

failCount = elements.AsParallel()
                            .Sum(element => IntegrationClass.DoRequest(element.ToString()));

它结果非常简单,闪电般快速。

我希望这有助于其他人尝试做同样的事情!