TPL架构问题

时间:2011-06-10 15:03:58

标签: c# .net task-parallel-library

我目前正在开展一个项目,我们面临着并行处理项目的挑战。到目前为止没什么大不了的;) 现在来问题了。我们有一个ID列表,我们定期(每2秒)为每个ID调用StoredProcedure。 每个项目需要单独检查2秒,因为它们是在运行时添加和删除的。 此外,我们希望配置最大并行度,因为DB不应同时充满300个线程。 正在处理的项目在完成上一次执行之前不应重新安排进行处理。原因是我们想要防止排队很多项目,以防数据库出现延迟。

现在我们正在使用一个自行开发的组件,它有一个主线程,可定期检查需要安排处理的项目。一旦它有了列表,它就会删除那些基于自定义IOCP的线程池,然后使用等待句柄等待正在处理的项目。然后下一次迭代开始。 IOCP因为它提供的工作窃取。

我想用TPL / .NET 4版本替换这个自定义实现,我想知道你将如何解决它(理想的简单和易读/可维护)。 我知道这篇文章:http://msdn.microsoft.com/en-us/library/ee789351.aspx,但它只是限制了使用的线程数量。叶子偷窃,定期执行物品......

理想情况下,它将成为一个通用组件,可用于需要定期为项目列表完成的所有任务。

欢迎任何输入, TIA 马丁

2 个答案:

答案 0 :(得分:9)

我认为你实际上不需要因为直接TPL Tasks而沮丧。对于初学者,我会在BlockingCollection(默认值)周围设置一个ConcurrentQueueBlockingCollection上没有设置BoundedCapacity来存储需要处理的ID。

// Setup the blocking collection somewhere when your process starts up (OnStart for a Windows service)
BlockingCollection<string> idsToProcess = new BlockingCollection<string>();

从那里开始,我只会对Parallel::ForEach返回的枚举使用BlockingCollection::GetConsumingEnumerable。在ForEach来电中,您将设置ParallelOptions::MaxDegreeOfParallelismForEach的正文中,您将执行存储过程。

现在,一旦存储过程执行完成,您就说不想重新安排执行至少两秒钟。没问题,安排一个带有回调的System.Threading.Timer,只需将ID添加回提供的回调中的BlockingCollection

Parallel.ForEach(
    idsToProcess.GetConsumingEnumerable(),
    new ParallelOptions 
    { 
        MaxDegreeOfParallelism = 4 // read this from config
    },
    (id) =>
    {
       // ... execute sproc ...

       // Need to declare/assign this before the delegate so that we can dispose of it inside 
       Timer timer = null;

       timer = new Timer(
           _ =>
           {
               // Add the id back to the collection so it will be processed again
               idsToProcess.Add(id);

               // Cleanup the timer
               timer.Dispose();
           },
           null, // no state, id wee need is "captured" in the anonymous delegate
           2000, // probably should read this from config
           Timeout.Infinite);
    }

最后,当进程关闭时,您将调用BlockingCollection::CompleteAdding,以便使用停止阻塞和完成处理可枚举的数据,并且Parallel :: ForEach将退出。例如,如果这是Windows服务,您可以在OnStop中执行此操作。

// When ready to shutdown you just signal you're done adding
idsToProcess.CompleteAdding();

<强>更新

您在评论中提出了一个有效的问题,即您可能在任何给定点处理大量ID,并担心每个ID的计时器会产生过多的开销。我绝对同意这一点。因此,在您同时处理大量ID的情况下,我将从使用每个ID的计时器更改为使用另一个队列来保持&#34;睡眠&#34;由单个短间隔计时器监视的ID。首先,你需要一个ConcurrentQueue来放置睡着的ID:

ConcurrentQueue<Tuple<string, DateTime>> sleepingIds = new ConcurrentQueue<Tuple<string, DateTime>>();

现在,我在这里使用两部分Tuple进行说明,但您可能希望为其创建一个更强类型的结构(或者至少使用{{1}创建别名}声明)以提高可读性。元组具有id和DateTime,表示它何时被放入队列。

现在您还要设置将监视此队列的计时器:

using

然后您只需更改Timer wakeSleepingIdsTimer = new Timer( _ => { DateTime utcNow = DateTime.UtcNow; // Pull all items from the sleeping queue that have been there for at least 2 seconds foreach(string id in sleepingIds.TakeWhile(entry => (utcNow - entry.Item2).TotalSeconds >= 2)) { // Add this id back to the processing queue idsToProcess.Enqueue(id); } }, null, // no state Timeout.Infinite, // no due time 100 // wake up every 100ms, probably should read this from config ); 即可执行以下操作,而不是为每个设置计时器:

Parallel::ForEach

答案 1 :(得分:1)

这与您在问题中已经说过的方法非常类似,但是在TPL任务中也是如此。任务只是将其自身添加回要完成的事项列表。

在这个例子中使用锁定普通列表相当丑陋,可能想要一个更好的集合来保存要安排的事物列表

// Fill the idsToSchedule
for (int id = 0; id < 5; id++)
{
    idsToSchedule.Add(Tuple.Create(DateTime.MinValue, id));
}

// LongRunning will tell TPL to create a new thread to run this on
Task.Factory.StartNew(SchedulingLoop, TaskCreationOptions.LongRunning);

启动SchedulingLoop,它实际上执行检查,如果它已经运行了两秒钟

// Tuple of the last time an id was processed and the id of the thing to schedule
static List<Tuple<DateTime, int>> idsToSchedule = new List<Tuple<DateTime, int>>();
static int currentlyProcessing = 0;
const int ProcessingLimit = 3;

// An event loop that performs the scheduling
public static void SchedulingLoop()
{
    while (true)
    {
        lock (idsToSchedule)
        {
            DateTime currentTime = DateTime.Now;
            for (int index = idsToSchedule.Count - 1; index >= 0; index--)
            {
                var scheduleItem = idsToSchedule[index];
                var timeSincePreviousRun = (currentTime - scheduleItem.Item1).TotalSeconds;

                // start it executing in a background task
                if (timeSincePreviousRun > 2 && currentlyProcessing < ProcessingLimit)
                {
                    Interlocked.Increment(ref currentlyProcessing);

                    Console.WriteLine("Scheduling {0} after {1} seconds", scheduleItem.Item2, timeSincePreviousRun);

                    // Schedule this task to be processed
                    Task.Factory.StartNew(() =>
                        {
                            Console.WriteLine("Executing {0}", scheduleItem.Item2);

                            // simulate the time taken to call this procedure
                            Thread.Sleep(new Random((int)DateTime.Now.Ticks).Next(0, 5000) + 500);

                            lock (idsToSchedule)
                            {
                                idsToSchedule.Add(Tuple.Create(DateTime.Now, scheduleItem.Item2));
                            }

                            Console.WriteLine("Done Executing {0}", scheduleItem.Item2);
                            Interlocked.Decrement(ref currentlyProcessing);
                        });

                    // remove this from the list of things to schedule
                    idsToSchedule.RemoveAt(index);
                }
            }
        }

        Thread.Sleep(100);
    }
}