当必须锁定每个列表元素时,多个线程应该如何访问列表?
How should multiple threads access a list when each list element must be locked?
我有一个 "Module" 类的列表,List<Module> modules
。这些模块每个都包含自己的 public 对象,在访问数据时用作锁。假设我有几个线程在随机时间对这些模块执行处理。目前我让每个线程按顺序对模块执行处理,如下所示:
foreach (Module module in modules)
{
lock (module.Locker)
{
//Do stuff
}
}
到目前为止这一切都很好,但我觉得有很多不必要的等待。例如,如果两个线程一个接一个地启动,但第一个正在执行繁重的处理而第二个不是,则第二个线程将不得不在第一个线程执行其处理时等待每个模块。
那么问题来了:是否有 "proper" 或 "most efficient" 方法来锁定列表中的元素?我打算这样做:
foreach (Module module in modules.Randomize())
{
lock (module.Locker)
{
//Do stuff
}
}
其中"Randomize()"只是一种扩展方法,returns列表中的元素以随机顺序排列。但是,我想知道是否有比随机更好的方法?
lock
代表Monitor.Enter
,你可以使用Monitor.TryEnter
检查是否已经获取了锁,并以某种方式跳过这个元素并尝试获取另一个。
如果多个线程正在处理 相同 有序的项目列表,将会有开销,所以 Randomize
的想法似乎是一个不错的想法(除非与处理相比重新排序是昂贵的本身,或者列表可以在处理时更改等)。
完全另一种可能性是为每个线程准备队列(从列表中),这种方式不会有交叉等待(或等待将被最小化)。结合 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))
}
假设锁内的工作量巨大且竞争激烈。我引入了创建新 List<T>
和从中删除项目的额外开销。
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--;
}
}
}
这个方法所做的是获取传入的模块的副本,然后尝试获取锁,如果无法获取锁(因为另一个线程拥有它),它会跳过并继续。完成列表后,它再次查看是否有另一个线程释放了锁,如果没有再次跳过它并继续前进。这个循环一直持续到我们处理完列表中的所有模块。
这样,我们就不会等待任何争用的锁,我们只会继续处理未被另一个线程锁定的模块。
不确定我是否完全遵循,但据我所知,您的目标是定期对每个模块执行操作,并且您想使用多个线程,因为这些操作很耗时。如果是这种情况,我将有一个 单线程 定期检查所有模块并让该线程使用 TPL 来分散工作负载,如下所示:
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
}
}
即每个模块都应该是线程安全的,并负责自己的锁定。
我有一个 "Module" 类的列表,List<Module> modules
。这些模块每个都包含自己的 public 对象,在访问数据时用作锁。假设我有几个线程在随机时间对这些模块执行处理。目前我让每个线程按顺序对模块执行处理,如下所示:
foreach (Module module in modules)
{
lock (module.Locker)
{
//Do stuff
}
}
到目前为止这一切都很好,但我觉得有很多不必要的等待。例如,如果两个线程一个接一个地启动,但第一个正在执行繁重的处理而第二个不是,则第二个线程将不得不在第一个线程执行其处理时等待每个模块。
那么问题来了:是否有 "proper" 或 "most efficient" 方法来锁定列表中的元素?我打算这样做:
foreach (Module module in modules.Randomize())
{
lock (module.Locker)
{
//Do stuff
}
}
其中"Randomize()"只是一种扩展方法,returns列表中的元素以随机顺序排列。但是,我想知道是否有比随机更好的方法?
lock
代表Monitor.Enter
,你可以使用Monitor.TryEnter
检查是否已经获取了锁,并以某种方式跳过这个元素并尝试获取另一个。
如果多个线程正在处理 相同 有序的项目列表,将会有开销,所以 Randomize
的想法似乎是一个不错的想法(除非与处理相比重新排序是昂贵的本身,或者列表可以在处理时更改等)。
完全另一种可能性是为每个线程准备队列(从列表中),这种方式不会有交叉等待(或等待将被最小化)。结合 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))
}
假设锁内的工作量巨大且竞争激烈。我引入了创建新 List<T>
和从中删除项目的额外开销。
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--;
}
}
}
这个方法所做的是获取传入的模块的副本,然后尝试获取锁,如果无法获取锁(因为另一个线程拥有它),它会跳过并继续。完成列表后,它再次查看是否有另一个线程释放了锁,如果没有再次跳过它并继续前进。这个循环一直持续到我们处理完列表中的所有模块。
这样,我们就不会等待任何争用的锁,我们只会继续处理未被另一个线程锁定的模块。
不确定我是否完全遵循,但据我所知,您的目标是定期对每个模块执行操作,并且您想使用多个线程,因为这些操作很耗时。如果是这种情况,我将有一个 单线程 定期检查所有模块并让该线程使用 TPL 来分散工作负载,如下所示:
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
}
}
即每个模块都应该是线程安全的,并负责自己的锁定。