在 C++ 中使用槽向量和互斥量的线程的互斥槽分配器
Mutually exclusive slot allocator for threads using a vector of slots and mutexes in C++
如果这篇文章已经发布到其他地方,我很抱歉,但我找不到任何匹配的内容。
在class中我们找到了一个预先设计的槽分配器,它存储了指定数量的槽,这些槽可以被线程获取。如果需要一个插槽,则该插槽应该工作,否则它将被阻塞,直到另一个线程释放。代码如下所示:
struct slot_allocator_mutexes
{
private:
int num_slots = 8;
vector<bool> slots;
vector<mutex> mutexes;
public:
slot_allocator_mutexes() : slots(num_slots, false), mutexes(num_slots) {}
int acquire_slot()
{
while(true)
{
for (int i = 0; i < num_slots; ++i)
{
mutexes[i].lock();
vector<bool>::reference slot_ref = slots[i];
if (slot_ref == false)
{
slot_ref = true;
mutexes[i].unlock();
return i;
}
mutexes[i].unlock();
}
}
}
void release_slot(int slot)
{
mutexes[slot].lock();
assert(slots[slot] == true);
slots[slot] = false;
mutexes[slot].unlock();
}
};
并将通过
调用
for (int t = 0; t < thread_numbers; ++t)
{
threads.push_back(thread([&]() {
for (int r = 0; r < repeats; ++r)
{
int slot = alloc.acquire_slot();
cout_lock.lock();
cout << "iteration num: " << r << endl;
cout << "Current slot: " << slot << " in thread number " << pthread_self() << endl;
cout_lock.unlock();
alloc.release_slot(slot);
}
}));
}
澄清一下:我知道可能会有更简洁的实现 - 不过这不是我的问题。
我的任务是查明这是否是分配器的正确线程安全实现。使用 assert assert(slots[slot] == true);
我很快发现,有些东西不起作用。经过研究和多种测试方法后,我必须承认我没有想法...
我不明白为什么这不起作用。我最初的想法是 mutexes[i].lock();
可能是错误,但由于这只是一个读取操作,所以这不是真正的答案。
谢谢。
只使用一个互斥量和一个 condition variable 怎么样?请参阅 cppreference 中的示例并考虑重新设计您的解决方案。
std::vector<bool>
作为动态位集有效地实现。标准没有指定具体的实现方式,但将实现为某种整数类型(例如 std::uint32_t
)的连续序列,其中每个位代表 vector
中的一个 bool
值].
因此,将任何一个值设置为 true
或 false
实际上是在修改更大对象的一部分:内部整数对象。您不能简单地锁定正在修改的单个位并期望 thread-safety,因为多个线程可能同时改变该整数 。
正确同步的唯一方法是不使用 vector<bool>
而是使用类似 std::vector<std::uint8_t>
的东西,其中每个 uint8_t
都是一个可以独立锁定的唯一对象并修改。
举一个具体的例子,假设 vector<bool>
中使用的底层 int 是 uint32_t
—— 这样您一次可以在 32 位上进行操作。
现在假设设置一个位是由这三个操作执行的:
- 将整数值加载到寄存器
- 执行或操作将位设置为
1
,并且
- 将整数值存回内存
如果整数从值 0x0
开始(所有位都设置为零),并且我们有两个线程试图同时设置位 0
和 1
,则不会 被互斥锁锁定,因为每个位都有一个单独的互斥锁。相反,我们可能会看到以下竞争条件:
Thread 1 Thread 2
Lock mutex for bit 0
Lock mutex for bit 1
Load <int> into register
Load same <int> into register
OR 0x1 with the register
OR 0x2 with the register
Write register back to mem
Write register back to mem
Unlock mutex for bit 0
unlock mutex for bit 1
请注意,在这种情况下,线程 2 将 破坏 线程 1 的值。形式上,这实际上只是未定义的行为,我们在这一点上看到的任何东西都完全没有被标准定义。如果顺序正确,我们可以看到 0x1
,我们可以看到 0x2
,或者我们可以看到 0x3
。从技术上讲,对于未定义的行为,一切皆有可能。
如果这篇文章已经发布到其他地方,我很抱歉,但我找不到任何匹配的内容。
在class中我们找到了一个预先设计的槽分配器,它存储了指定数量的槽,这些槽可以被线程获取。如果需要一个插槽,则该插槽应该工作,否则它将被阻塞,直到另一个线程释放。代码如下所示:
struct slot_allocator_mutexes
{
private:
int num_slots = 8;
vector<bool> slots;
vector<mutex> mutexes;
public:
slot_allocator_mutexes() : slots(num_slots, false), mutexes(num_slots) {}
int acquire_slot()
{
while(true)
{
for (int i = 0; i < num_slots; ++i)
{
mutexes[i].lock();
vector<bool>::reference slot_ref = slots[i];
if (slot_ref == false)
{
slot_ref = true;
mutexes[i].unlock();
return i;
}
mutexes[i].unlock();
}
}
}
void release_slot(int slot)
{
mutexes[slot].lock();
assert(slots[slot] == true);
slots[slot] = false;
mutexes[slot].unlock();
}
};
并将通过
调用 for (int t = 0; t < thread_numbers; ++t)
{
threads.push_back(thread([&]() {
for (int r = 0; r < repeats; ++r)
{
int slot = alloc.acquire_slot();
cout_lock.lock();
cout << "iteration num: " << r << endl;
cout << "Current slot: " << slot << " in thread number " << pthread_self() << endl;
cout_lock.unlock();
alloc.release_slot(slot);
}
}));
}
澄清一下:我知道可能会有更简洁的实现 - 不过这不是我的问题。
我的任务是查明这是否是分配器的正确线程安全实现。使用 assert assert(slots[slot] == true);
我很快发现,有些东西不起作用。经过研究和多种测试方法后,我必须承认我没有想法...
我不明白为什么这不起作用。我最初的想法是 mutexes[i].lock();
可能是错误,但由于这只是一个读取操作,所以这不是真正的答案。
谢谢。
只使用一个互斥量和一个 condition variable 怎么样?请参阅 cppreference 中的示例并考虑重新设计您的解决方案。
std::vector<bool>
作为动态位集有效地实现。标准没有指定具体的实现方式,但将实现为某种整数类型(例如 std::uint32_t
)的连续序列,其中每个位代表 vector
中的一个 bool
值].
因此,将任何一个值设置为 true
或 false
实际上是在修改更大对象的一部分:内部整数对象。您不能简单地锁定正在修改的单个位并期望 thread-safety,因为多个线程可能同时改变该整数 。
正确同步的唯一方法是不使用 vector<bool>
而是使用类似 std::vector<std::uint8_t>
的东西,其中每个 uint8_t
都是一个可以独立锁定的唯一对象并修改。
举一个具体的例子,假设 vector<bool>
中使用的底层 int 是 uint32_t
—— 这样您一次可以在 32 位上进行操作。
现在假设设置一个位是由这三个操作执行的:
- 将整数值加载到寄存器
- 执行或操作将位设置为
1
,并且 - 将整数值存回内存
如果整数从值 0x0
开始(所有位都设置为零),并且我们有两个线程试图同时设置位 0
和 1
,则不会 被互斥锁锁定,因为每个位都有一个单独的互斥锁。相反,我们可能会看到以下竞争条件:
Thread 1 Thread 2
Lock mutex for bit 0
Lock mutex for bit 1
Load <int> into register
Load same <int> into register
OR 0x1 with the register
OR 0x2 with the register
Write register back to mem
Write register back to mem
Unlock mutex for bit 0
unlock mutex for bit 1
请注意,在这种情况下,线程 2 将 破坏 线程 1 的值。形式上,这实际上只是未定义的行为,我们在这一点上看到的任何东西都完全没有被标准定义。如果顺序正确,我们可以看到 0x1
,我们可以看到 0x2
,或者我们可以看到 0x3
。从技术上讲,对于未定义的行为,一切皆有可能。