Thread.Sleep阻止并行执行任务

时间:2011-09-26 20:54:49

标签: c# multithreading task-parallel-library plinq

我正在调用一个调用数据库的worker方法,然后迭代并生成并行处理的返回值。为了防止它锤击数据库,我在那里有一个Thread.Sleep来暂停执行到DB。但是,这似乎是在Parallel.ForEach中仍然发生的阻塞执行。实现这一目标以防止阻塞的最佳方法是什么?

private void ProcessWorkItems()
{
    _cancellation = new CancellationTokenSource();
    _cancellation.Token.Register(() => WorkItemRepository.ResetAbandonedWorkItems());

    Task.Factory.StartNew(() =>
        Parallel.ForEach(GetWorkItems().AsParallel().WithDegreeOfParallelism(10), workItem =>
        {
            var x = ItemFactory(workItem);
            x.doWork();
        }), _cancellation.Token);
}

private IEnumerable<IAnalysisServiceWorkItem> GetWorkItems()
{
    while (!_cancellation.IsCancellationRequested)
    {
        var workItems = WorkItemRepository.GetItemList(); //database call

        workItems.ForEach(item =>
        {
            item.QueueWorkItem(WorkItemRepository);
        });

        foreach (var item in workItems)
        {
            yield return item;
        }

        if (workItems.Count == 0)
        {
            Thread.Sleep(30000); //sleep this thread for 30 seconds if no work items.
        }
    }

    yield break;
}

编辑: 我改变它以包括答案,它仍然没有像我期望的那样工作。我将.AsParallel()。WithDegreeOfParallelism(10)添加到GetWorkItems()调用中。当我认为即使基本线程正在休眠时,Parallel仍应继续执行,我的期望是否正确?

实施例: 我有15个项目,它迭代并抓取10个项目并启动它们。当每个人完成时,它会从GetWorkItems请求另一个,直到它试图要求第16个项目。此时它应该停止尝试获取更多项目,但应继续处理项目11-15,直到完成。是应该如何并行工作?因为它目前没有这样做。它目前正在做的是当它完成6时,它会锁定后续的10个仍然在Parallel.ForEach中运行。

4 个答案:

答案 0 :(得分:8)

我建议您创建一个BlockingCollection(一个队列)的工作项,以及一个每隔30秒调用一次数据库的计时器来填充它。类似的东西:

BlockingCollection<WorkItem> WorkItems = new BlockingCollection<WorkItem>();

初始化时:

System.Threading.Timer WorkItemTimer = new Timer((s) =>
    {
        var items = WorkItemRepository.GetItemList(); //database call
        foreach (var item in items)
        {
            WorkItems.Add(item);
        }
    }, null, 30000, 30000);

这将每隔30秒向数据库查询一次。

要安排要处理的工作项,您有许多不同的解决方案。最接近你的是:

WorkItem item;

while (WorkItems.TryTake(out item, Timeout.Infinite, _cancellation))
{
    Task.Factory.StartNew((s) =>
        {
            var myItem = (WorkItem)s;
            // process here
        }, item);
}

这消除了任何线程中的阻塞,并让TPL决定如何最好地分配并行任务。

编辑:

实际上,更接近你所拥有的是:

foreach (var item in WorkItems.GetConsumingEnumerable(_cancellation))
{
    // start task to process item
}

您可以使用:

Parallel.Foreach(WorkItems.GetConsumingEnumerable(_cancellation).AsParallel ...

我不知道这是否有用或有多好。也许值得尝试一下 。 。

编辑结束

一般来说,我建议您将其视为生产者/消费者应用程序,生产者是定期查询数据库以获取新项目的线程。我的示例每隔N(本例中为30)秒查询数据库一次,如果平均每30秒就可以清空一次工作队列,这将很有效。从项目发布到数据库到结果之前,这将给出不到一分钟的平均延迟。

您可以降低轮询频率(以及延迟),但这会导致更多的数据库流量。

你也可以用它来获得更好的体验。例如,如果您在30秒后轮询数据库并获得大量项目,那么很可能您将很快获得更多,并且您将需要在15秒(或更短)内再次轮询。相反,如果您在30秒后轮询数据库并且什么也得不到,那么您可以在再次轮询之前等待更长时间。

您可以使用一次性计时器设置这种自适应轮询。也就是说,在创建计时器时为最后一个参数指定-1,这会导致它仅触发一次。您的计时器回调计算出下次轮询之前等待的时间,并调用Timer.Change以使用新值初始化计时器。

答案 1 :(得分:3)

您可以使用.WithDegreeOfParallelism()扩展方法强制PLinq同时运行任务。在C# Threading Handbook

中的呼叫阻止或I / O强化部分中有一个很好的示例

答案 2 :(得分:2)

你可能会对分区者犯规。

因为你传递的是IEnumerable,所以Parallel.ForEach将使用一个Chunk Partitioner,它可以尝试一次从一个块中的枚举中获取一些元素。但是你的IEnumerable.MoveNext可以睡觉,这会让事情变得不舒服。

您可以编写自己的分区程序,一次返回一个元素,但无论如何,我认为像Jim Mischel建议的生产者/消费者方法会更好。

答案 3 :(得分:0)

你想睡觉的目的是什么?据我所知,你试图避免敲击数据库调用。我不知道有更好的方法可以做到这一点,但理想情况是,在数据可用于处理之前,您的GetItemList调用会阻塞。