C# 当必须锁定每个列表元素时,多个线程应该如何访问列表?

C# 当必须锁定每个列表元素时,多个线程应该如何访问列表?,c#,list,concurrency,locking,C#,List,Concurrency,Locking,我有一个“模块”类列表,列出模块。这些模块各自包含自己的公共对象,在访问数据时用作锁。假设我有几个线程在随机时间对这些模块执行处理。目前,我让每个线程按顺序对模块执行处理,如下所示: foreach (Module module in modules) { lock (module.Locker) { //Do stuff } } Parallel.ForEach(modules, module => { lock(module.Locke

我有一个“模块”类列表,
列出模块
。这些模块各自包含自己的公共对象,在访问数据时用作锁。假设我有几个线程在随机时间对这些模块执行处理。目前,我让每个线程按顺序对模块执行处理,如下所示:

foreach (Module module in modules)
{
    lock (module.Locker)
    {
        //Do stuff
    }
}
Parallel.ForEach(modules, module =>
{
    lock(module.Locker)
    {

    }
});
到目前为止,这一切都很顺利,但我感觉有很多不必要的等待。例如,如果两个线程一个接一个地启动,但第一个线程正在执行繁重的处理,而第二个线程没有,那么第二个线程将不得不在第一个线程执行其处理时等待每个模块

这就是问题所在:是否有“适当”或“最有效”的方法来锁定列表中的元素?我打算这样做:

foreach (Module module in modules.Randomize())
{
    lock (module.Locker)
    {
        //Do stuff
    }
}

其中“Randomize()”只是一个扩展方法,它以随机顺序返回列表中的元素。然而,我想知道是否有比随机更好的方法?

lock
代表
监视器。输入
,可以用来检查是否已经获得了锁,并以某种方式跳过此元素并尝试使用另一个元素

如果多个线程正在处理同一个有序的项目列表,则会产生开销,因此使用
随机化
的想法似乎是一个好主意(除非重新排序比处理本身更昂贵,或者列表可以在处理时更改,等等)

另一种可能性是为每个线程准备队列(从列表中),这样就不会出现交叉等待(或者等待将最小化)。与
Monitor.TryEnter
相结合,这应该是一个最终的解决方案。不幸的是,我不知道如何准备这样的队列,也不知道如何跳过处理队列项,而将此留给您=P


下面是我的意思的一个片段:

foreach(var item in list)
    if(!item.Processed && Monitor.TryEnter(item.Locker))
        try
        {
            ... // do job
            item.Processed = true;
        }
        finally
        {
            Monitor.Exit(item.Locker))
        }

假设锁内的工作是巨大的,并且存在严重的争用。我引入了创建新的
列表
并从中删除项目的额外开销

public void ProcessModules(List<Module> modules)
{
    List<Module> myModules = new List<Module>(modules);//Take a copy of the list
    int index = myModules.Count - 1;
    while (myModules.Count > 0)
    {
        if (index < 0)
        {
            index = myModules.Count - 1;
        }

        Module module = myModules[index];
        if (!Monitor.TryEnter(module.Locker))
        {
            index--;
            continue;
        }

        try
        {
            //Do processing module
        }
        finally
        {
            Monitor.Exit(module.Locker);
            myModules.RemoveAt(index);
            index--;
        }
    }
}
public void ProcessModules(列出模块)
{
List myModules=新列表(模块);//复制列表
int index=myModules.Count-1;
而(myModules.Count>0)
{
如果(指数<0)
{
index=myModules.Count-1;
}
模块=我的模块[索引];
如果(!监视器TryEnter(模块锁定器))
{
索引--;
继续;
}
尝试
{
//Do处理模块
}
最后
{
监控出口(模块储物柜);
myModules.RemoveAt(索引);
索引--;
}
}
}
这个方法的作用是获取传入模块的副本,然后尝试获取锁,如果无法获取锁(因为另一个线程拥有它),它将跳过并继续。完成列表后,它会再次查看另一个线程是否释放了锁,如果没有,它会再次跳过锁并继续。这个循环一直持续到我们处理列表中的所有模块


这样,我们就不必等待任何争用的锁,我们只需继续处理没有被另一个线程锁定的模块。

我不确定我是否完全遵守,但是从我可以告诉你的情况来看,你的目标是周期性地对每个模块执行一些操作,你需要使用多个线程,因为这些操作非常耗时。如果是这种情况,我将使用一个单线程定期检查所有模块,并让该线程使用TPL来分配工作负载,如下所示:

foreach (Module module in modules)
{
    lock (module.Locker)
    {
        //Do stuff
    }
}
Parallel.ForEach(modules, module =>
{
    lock(module.Locker)
    {

    }
});
另一方面,关于锁的指导是,你锁定的对象应该是私有的,所以我可能会改为这样做:

Parallel.ForEach(modules, module => module.DoStuff());

// In the module implementation
private readonly object _lock = new object();

public void DoStuff()
{
    lock (this._lock)
    {
        // Do stuff here
    }
}

也就是说,每个模块都应该是线程安全的,并负责自己的锁定。

请查看线程池和阻塞队列/非阻塞队列。我觉得在你的程序中,对象的锁是不必要的,因为你似乎只是用它来告诉其他线程对象被占用了;阻塞/非阻塞队列有何帮助?队列中应该包含什么?所以如果其他线程已经在处理该元素,您希望线程跳过该元素?否则,我不明白你的问题。本质上,你只希望每个元素处理一次,但只处理一次,对吗?在这种情况下,只需将它们插入队列并调用线程池即可。您可以将模块上的操作拆分为读取和写入操作吗?如果是这样的话,您可以通过使用在任何时候都不会更改的列表来提高效率,所以这不是问题。嗯,我没有想到显示器的解决方案。由于“TryEnter”可能导致的问题,我总是尽量避免使用它,但在这种情况下,因为我将使用不同的元素,所以它可能会起作用。编辑:哎呀,我之前的回答很愚蠢,因为我在想别的事情。TryEnter实际上获得了锁,所以这可以很好地工作。这是。。。其实真的很聪明!我不知道“TryEnter”的存在;我查到的大多数东西都是出于某种原因而避开显示器的,所以我从来没有真正去研究过。我觉得他们应该在我的并行计算课程XD中教授这些访问模式。当然,在寻找空闲模块时,线程仍然有可能无限期地被卡住。它可能总是错过机会。我可以添加安全代码,我想如果满足一定数量的迭代,我只需切换到“顺序”。@limitlessinfinity如果线程被无限期锁定,这意味着你还有其他问题。我是说其他线程没有释放锁。您可以进一步优化此算法,以发现如果没有更多可用模块,但
列表仍不为空,则可以切换到监视器。输入`from
Monitor.TryEnter
。这样可以避免烧坏CPU,比如说公园里有一大堆永远开放的食品摊。比如说,一个饥饿的家伙不想排队等食物,所以