我目前正在开展一个项目,我们面临着并行处理项目的挑战。到目前为止没什么大不了的;) 现在来问题了。我们有一个ID列表,我们定期(每2秒)为每个ID调用StoredProcedure。 每个项目需要单独检查2秒,因为它们是在运行时添加和删除的。 此外,我们希望配置最大并行度,因为DB不应同时充满300个线程。 正在处理的项目在完成上一次执行之前不应重新安排进行处理。原因是我们想要防止排队很多项目,以防数据库出现延迟。
现在我们正在使用一个自行开发的组件,它有一个主线程,可定期检查需要安排处理的项目。一旦它有了列表,它就会删除那些基于自定义IOCP的线程池,然后使用等待句柄等待正在处理的项目。然后下一次迭代开始。 IOCP因为它提供的工作窃取。
我想用TPL / .NET 4版本替换这个自定义实现,我想知道你将如何解决它(理想的简单和易读/可维护)。 我知道这篇文章:http://msdn.microsoft.com/en-us/library/ee789351.aspx,但它只是限制了使用的线程数量。叶子偷窃,定期执行物品......
理想情况下,它将成为一个通用组件,可用于需要定期为项目列表完成的所有任务。
欢迎任何输入, TIA 马丁
答案 0 :(得分:9)
我认为你实际上不需要因为直接TPL Tasks
而沮丧。对于初学者,我会在BlockingCollection
(默认值)周围设置一个ConcurrentQueue
,BlockingCollection
上没有设置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::MaxDegreeOfParallelism
在ForEach
的正文中,您将执行存储过程。
现在,一旦存储过程执行完成,您就说不想重新安排执行至少两秒钟。没问题,安排一个带有回调的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);
}
}