C# 在某些任务非常昂贵的情况下,如何实现并行负载平衡?

C# 在某些任务非常昂贵的情况下,如何实现并行负载平衡?,c#,multithreading,parallel-processing,task-parallel-library,C#,Multithreading,Parallel Processing,Task Parallel Library,我有一个需要处理的对象列表。假设这个列表是所有客户的列表,我需要对所有客户执行CPU密集型计算。虽然在这个计算之前和之后,我需要获取数据并将数据提交回数据库,所以这不仅仅是一个CPU任务 所以我要做的是 Parallel.ForEach(列表、动作) 实际上,行动就是 1 Fetch customer data 2 Process calculate (time and memory intensive task) 3 Commit back customer data 该代码过去运行良好,但

我有一个需要处理的对象列表。假设这个列表是所有客户的列表,我需要对所有客户执行CPU密集型计算。虽然在这个计算之前和之后,我需要获取数据并将数据提交回数据库,所以这不仅仅是一个CPU任务

所以我要做的是

Parallel.ForEach(列表、动作)

实际上,行动就是

1 Fetch customer data
2 Process calculate (time and memory intensive task)
3 Commit back customer data
该代码过去运行良好,但最近当处理多个记录数非常多的客户时,我们会使系统内存不足

那么,有没有一种方法可以平衡这个负载呢?大多数客户的处理速度都很快,但占用所有资源的却很少。我能避免他们几个一起跑吗


我可以实现这一点的一种方法是根据大小对列表进行排序,然后尝试选择第一项和最后一项,并自己控制并行性,但我想看看这里有哪些选项。

因为你说在实际完成计算之前,你已经得到了计算大小的近似值,这就大大简化了操作。此时,您只需要一个同步原语,它不限制要执行的操作数,而是具有一些总权重值,并确保当前运行的所有操作的总和小于指定的权重值。然后,您可以请求一个给定的操作以给定的权重值运行,直到有足够的未使用的权重值供其使用,它才会真正运行

没有一个现有的原语可以做到这一点(信号量非常接近,但并不完全如此)。但是,您可以相当容易地从现有的同步原语中创建一个同步原语

public class WeightedSemaphore
{
    public WeightedSemaphore(int totalWeight)
    {
        currentWeight = TotalWeight = totalWeight;
    }

    private ManualResetEvent signal = new ManualResetEvent(false);
    private int currentWeight;
    public int TotalWeight { get; }
    public int CurrentWeight { get { lock (signal) return currentWeight; } }

    public void Wait(int weight)
    {
        while (true)
        {
            lock (signal)
            {
                if (currentWeight >= weight)
                {
                    currentWeight -= weight;
                    return;
                }
            }

            signal.Reset();
            signal.WaitOne();
        }
    }
    public void Release(int weight)
    {
        lock (signal)
        {
            currentWeight += weight;
            signal.Set();
        }
    }
}
现在,您可以完成每个操作,确保在完成工作之前,他们会等待并提供其“大小”值。从这里开始,只需进行一些实验,就可以计算出当前系统能够支持的总重量


请注意,这样做的一个副作用是,您会发现更快的操作往往会更快地获得优先级。当释放出一些空间时,较短的操作更有可能使用其中的内容运行,这意味着它将在更昂贵的操作开始运行之前保留该空间。在许多情况下,这实际上是一个令人满意的特性,因为当您将更快的操作优先于更昂贵的操作时,平均响应时间实际上会下降。

您可以监视内存使用情况,并在超出限制时暂停几位客户,但这很糟糕。您是否尝试过限制并行性@杰米克:那真的没用。问题在于,这些操作并不等价。有些是大的,有些是小的。很多小的操作都可以一起运行,即使是非常少的大项目一起运行也是一个问题。这是正确的@servy有可能将大客户分成多个区块吗?这取决于你们在记录上所做的计算的性质。谢谢servy!这实际上看起来是一种更干净的基于体重的隔离方式。将实现此功能,并让您知道它的性能。谢谢信号灯为什么不在那儿?信号量LIM不仅非常有效,支持异步等待,而且还提供相同的接口。信号量并不意味着您必须只获取或释放一个令牌。这里的问题是,很容易获得一个livelock——昂贵的操作永远不会执行,因为短而便宜的操作会使资源繁忙。修复这一问题要困难得多,但如果吞吐量足够高,则可能不是问题。@Voo问题在于它不提供允许您同时获取多个锁的操作。比如说,获取信号量的20个锁中的10个锁的唯一方法是在循环中等待10次,但如果这样做,可能会导致死锁。这种模式也不能很好地适应更大的权重值。如果
Wait
有一个重载,可以接受许多要取出的锁的实例(
Release
已经有这样一个重载),那么它将是合适的。@Voo在这种特殊情况下,缺乏更昂贵的技术不是问题,因为有固定数量的操作要执行,而不是一个恒定的操作流来执行,因此优先考虑更快的操作是一个优势,而不是劣势。对于恒定的操作流(您期望该流的吞吐量不断平均机器的容量,从而提供饥饿的可能性),确实需要更复杂的调度系统。然而,这个问题的需要并不需要这些。@Servy啊,使用ints和TimeSpan表示超时的问题,是的,我认为
Wait(int)
Release(int)
的相应操作。