我有一个线程池,其中包含一些线程(例如,尽可能多的内核),它们可以处理许多对象,例如数千个对象。通常我会给每个对象一个互斥锁以保护对其内部的访问,在我工作时将其锁定,然后释放它。当两个线程尝试访问同一个对象时,其中一个线程必须等待。
现在我想保存一些资源并且可以扩展,因为可能有数千个对象,但仍然只有一大堆线程。我正在考虑一个类设计,其中线程具有某种互斥或锁定对象,并在访问对象时将锁分配给对象。这样可以节省资源,因为我只拥有与线程一样多的锁定对象。
现在是编程部分,我想将这个设计转移到代码中,但不知道从哪里开始。我正在使用C ++进行编程,并希望尽可能使用Boost类,但是处理这些特殊要求的自编写的类是可以的。我该如何实现呢?
我的第一个想法是每个线程都有一个boost :: mutex对象,每个对象都有一个最初未设置的boost :: shared_ptr(或NULL)。现在,当我想访问该对象时,我通过创建一个scoped_lock对象并将其分配给shared_ptr来锁定它。当shared_ptr已经设置好后,我等待当前的锁定。这个想法听起来像一堆充满竞争条件,所以我有点放弃它。还有另一种方法来完成这个设计吗?一种完全不同的方式?
编辑: 以上描述有点抽象,所以让我添加一个具体的例子。想象一下拥有许多物体的虚拟世界(想想> 100.000)。移动世界的用户可以穿越世界并修改物体(例如在怪物身上射箭)。当只使用一个线程时,我很擅长一个工作队列,其中对象的修改排队。不过,我想要一个更具伸缩性的设计。如果128个核心处理器可用,我想使用全部128个,所以使用这个数量的线程,每个线程都有工作队列。一种解决方案是使用空间分离,例如,使用锁定区域。这可以减少使用的锁的数量,但是如果有一个设计可以节省尽可能多的锁,我会更感兴趣。
答案 0 :(得分:4)
您可以使用互斥锁池而不是为每个资源分配一个互斥锁,或者为每个线程分配一个互斥锁。当请求互斥锁时,首先检查有问题的对象。如果它已经标记了互斥锁,则阻止该互斥锁。如果没有,请为该对象分配一个互斥锁并发出信号,将互斥锁从池中取出。一旦互斥锁无信号,请清除插槽并将互斥锁返回池中。
答案 1 :(得分:3)
在不知情的情况下,您所寻找的是软件事务内存(STM)。
STM系统在内部管理所需的锁,以确保ACI属性(Atomic,Consistent,Isolated)。这是一项研究活动。你可以找到很多STM库;特别是我正在研究Boost.STM(该库尚未进行beta测试,文档不是最新的,但你可以玩)。还有一些编译器正在引入TM(如Intel,IBM和SUN编译器)。您可以从here
获取草案规范我们的想法是确定关键区域如下
transaction {
// transactional block
}
让STM系统管理所需的锁,只要它确保ACI属性。
使用Boost.STM方法可以编写
之类的内容int inc_and_ret(stm::object<int>& i) {
BOOST_STM_TRANSACTION {
return ++i;
} BOOST_STM_END_TRANSACTION
}
您可以看到一对BOOST_STM_TRANSACTION / BOOST_STM_END_TRANSACTION作为确定范围隐式锁定的方法。
这个伪透明度的成本是每个stm :: object的4个元数据字节。
即使这与您的初始设计相差甚远,我也真的认为这是您的目标和初步设计背后的原因。
答案 2 :(得分:1)
我怀疑有任何干净的方法来完成你的设计。将互斥锁分配给对象的问题看起来就像是要修改对象的内容 - 所以你需要一个互斥锁来保护对象免受几个线程的攻击,这些线程试图立即为它分配互斥锁,这样就可以保留你的第一个互斥锁分配安全,你需要另一个互斥锁来保护第一个。
就个人而言,我认为你想要治愈的东西可能首先不是问题。在我花了很多时间尝试修复它之前,我会做一些测试,看看你在每个对象中包含一个Mutex并完成它后会丢失什么(如果有的话)。我怀疑你是否还需要更进一步。
如果你需要做的不仅仅是我想要拥有一个线程安全的对象池,并且线程想要对一个对象进行操作,它必须从该池获得所有权。获取所有权的调用将释放当前由请求线程拥有的任何对象(以避免死锁),然后赋予它所请求对象的所有权(如果该对象当前由另一个线程拥有则阻止)。对象池管理器可能自己在一个线程中运行,自动序列化对池管理的所有访问,因此池管理代码可以避免必须锁定对变量的访问,告诉它当前拥有哪个对象等。
答案 3 :(得分:1)
就个人而言,这就是我要做的。你有很多对象,都可能有某种键,比如名字。因此,请按以下人员名单列出:
Bill Clinton
Bill Cosby
John Doe
Abraham Lincoln
Jon Stewart
所以现在你要创建一些列表:比如每个字母的一个字母。比尔和比尔将列入一个名单,约翰,乔恩亚伯拉罕都是他们自己。
每个列表都将被分配给一个特定的线程 - 访问必须通过该线程(您必须将操作编组到该线程上的对象 - 很好地使用仿函数)。那么你只有两个地方可以锁定:
thread() {
loop {
scoped_lock lock(list.mutex);
list.objectAccess();
}
}
list_add() {
scoped_lock lock(list.mutex);
list.add(..);
}
将锁定保持在最低限度,如果您仍然进行大量锁定,则可以优化对列表中对象执行的迭代次数,从1到5,以最大限度地减少获取所花费的时间锁。如果您的数据集增长或按数字键入,您可以执行任意数量的隔离数据以将锁定保持在最低限度。
答案 4 :(得分:1)
听起来像你需要一个工作队列。如果工作队列上的锁定成为瓶颈,您可以切换它,以便每个线程都有自己的工作队列,然后某种调度程序会将传入的对象提供给线程,并且工作量最少。从那以后的另一个层次是工作窃取,其中已经用完工作的线程查看其他线程的工作队列。(参见英特尔的线程构建块库。)
答案 5 :(得分:0)
如果我正确地跟着你......
struct table_entry {
void * pObject; // substitute with your object
sem_t sem; // init to empty
int nPenders; // init to zero
};
struct table_entry * table;
object_lock (void * pObject) {
goto label; // yes it is an evil goto
do {
pEntry->nPenders++;
unlock (mutex);
sem_wait (sem);
label:
lock (mutex);
found = search (table, pObject, &pEntry);
} while (found);
add_object_to_table (table, pObject);
unlock (mutex);
}
object_unlock (void * pObject) {
lock (mutex);
pEntry = remove (table, pObject); // assuming it is in the table
if (nPenders != 0) {
nPenders--;
sem_post (pEntry->sem);
}
unlock (mutex);
}
上述应该有效,但确实有一些潜在的缺点,例如......
但是,根据您的设置,这些潜在的缺点可能并不重要。
希望这有帮助。
答案 6 :(得分:0)
我们对类似模型感兴趣。我们考虑的解决方案是拥有全局(或共享)锁,但以下列方式使用:
虽然我们每次更改此变量的值时都需要锁定互斥锁。所以有很多锁定和解锁但你不需要长时间保持锁定。
使用“共享”锁定,您可以使用一个锁定应用于多个项目。您可以使用某种“哈希”函数来确定哪个互斥/条件变量适用于此特定条目。
答案 7 :(得分:0)
在@ JohnDibling的帖子下回答以下问题。
你实施了这个解决方案吗?我有一个类似的问题,我想知道你是如何解决将mutex释放回池的问题。我的意思是,当你释放互斥锁时,你怎么知道如果你不知道另一个线程是否持有它,它可以安全地重新排队?
by @LeonardoBernardini
我目前正试图解决同样的问题。我的方法是使用计数器字段和真实资源互斥字段创建自己的互斥结构(称为counterMutex)。因此,每次尝试锁定counterMutex时,首先递增计数器然后锁定基础互斥锁。当你完成它之后,你减少coutner并解锁互斥锁,之后检查计数器是否为零,这意味着没有其他线程试图获取锁。如果是这样,将counterMutex放回池中。操纵柜台时是否存在竞争条件?你可能会问。答案是不。请记住,您有一个全局互斥锁,以确保一次只能有一个线程访问coutnerMutex。