yield和Parallel.ForEach的奇怪行为

时间:2014-09-23 00:32:39

标签: c# .net multithreading

在工作中,我们的一个进程使用SQL数据库表作为队列。我一直在设计一个队列读取器来检查表的排队工作,在工作开始时更新行状态,并在工作完成时删除行。我正在使用Parallel.Foreach为每个进程提供自己的线程,并将MaxDegreeOfParallelism设置为4.

当队列阅读器启动时,它会检查任何未完成的工作并将工作加载到列表中,然后它会使用该列表执行Concat,并返回一个运行于IEnumerable的方法。无限循环检查要做的新工作。我们的想法是,应首先处理未完成的工作,然后在线程可用时处理新工作。但是我所看到的是FetchQueuedWork会将队列表中的几十行立即更改为“正在处理”,但一次只能处理几个项目。

我期望发生的事情是FetchQueuedWork只会在Parallel.Foreach中打开一个广告位时获得新工作并更新表格。对我来说真正奇怪的是它的行为与我在本地开发人员环境中运行代码时的预期完全相同,但在生产中我遇到了上述问题。

我正在使用.Net 4.以下是代码:

public void Go()
{
    List<WorkData> unfinishedWork = WorkData.LoadUnfinishedWork();
    IEnumerable<WorkData> work = unfinishedWork.Concat(FetchQueuedWork());     
    Parallel.ForEach(work, new ParallelOptions { MaxDegreeOfParallelism = 4 }, DoWork);
}

private IEnumerable<WorkData> FetchQueuedWork()
{
    while (true)
    {
        var workUnit = WorkData.GetQueuedWorkAndSetStatusToProcessing();
        yield return workUnit;
    }
}

private void DoWork(WorkData workUnit)
{
    if (!workUnit.Loaded)
    {
        System.Threading.Thread.Sleep(5000);
        return;
    }
    Work();
}

2 个答案:

答案 0 :(得分:3)

我怀疑默认(发布模式?)行为是缓冲输入。您可能需要创建自己的分区程序并将其传递给NoBuffering选项:

List<WorkData> unfinishedWork = WorkData.LoadUnfinishedWork();
IEnumerable<WorkData> work = unfinishedWork.Concat(FetchQueuedWork());     
var options = new ParallelOptions { MaxDegreeOfParallelism = 4 };
var partitioner = Partitioner.Create(work, EnumerablePartitionerOptions.NoBuffering);
Parallel.ForEach(partioner, options, DoWork);

答案 1 :(得分:3)

对于.NET 4.5,Blorgbeard的解决方案是正确的 - 放下手。

如果您受限于.NET 4,您有几个选择:

  • Parallel.ForEach替换为work.AsParallel().WithDegreeOfParallelism(4).ForAll(DoWork)。 PLINQ在缓冲项目方面更加保守,所以这应该可以解决问题。

  • 编写自己的可枚举分区程序(祝你好运)。

  • 创建一个基于信号量的基于信号量的黑客,例如:

为了简洁而使用的副作用Select

public void Go()
{
    const int MAX_DEGREE_PARALLELISM = 4;

    using (var semaphore = new SemaphoreSlim(MAX_DEGREE_PARALLELISM, MAX_DEGREE_PARALLELISM))
    {
        List<WorkData> unfinishedWork = WorkData.LoadUnfinishedWork();

        IEnumerable<WorkData> work = unfinishedWork
            .Concat(FetchQueuedWork())
            .Select(w =>
            {
                // Side-effect: bad practice, but easier
                // than writing your own IEnumerable.
                semaphore.Wait();

                return w;
            });

        // You still need to specify MaxDegreeOfParallelism
        // here so as not to saturate your thread pool when
        // Parallel.ForEach's load balancer kicks in.
        Parallel.ForEach(work, new ParallelOptions { MaxDegreeOfParallelism = MAX_DEGREE_PARALLELISM }, workUnit =>
        {
            try
            {
                this.DoWork(workUnit);
            }
            finally
            {
                semaphore.Release();
            }
        });
    }
}