在 C++ 中使用插槽和互斥体向量的线程的互斥插槽分配器

Posted

技术标签:

【中文标题】在 C++ 中使用插槽和互斥体向量的线程的互斥插槽分配器【英文标题】:Mutually exclusive slot allocator for threads using a vector of slots and mutexes in C++ 【发布时间】:2020-11-09 18:46:46 【问题描述】:

如果这已发布在其他任何地方,我非常抱歉,但我找不到任何匹配的内容。

在一个类中,我们发现了一个预先设计的槽分配器,它存储了线程可以获取的指定数量的槽。如果需要一个槽,则该槽应该工作,否则它会被阻塞,直到另一个线程释放。代码如下所示:

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(slots[slot] == true); 我很快就发现,有些东西不起作用。在进行了研究和多种测试方法之后,我必须承认我没有想法......

我不明白为什么这不起作用。我最初的想法是mutexes[i].lock(); 可能是问题所在,但由于这只是一个读取操作,所以这并不是真正的答案。

谢谢。

【问题讨论】:

您似乎没有向我们提供所有代码。 m 是什么?我看到m.unlock()。此外,您似乎在构造函数中命名为 mutexes locks。请确保您的最小可重现示例没有这些差异,否则它实际上只是一个猜谜游戏 @Human-Compiler 对这些错误非常抱歉,我更改了一些代码并且从那时起没有编译。现在应该可以工作了 您还没有发布minimal reproducible example。无论如何,答案实际上是一个非常奇怪的 c++ 怪癖,我相信一些 c++ 标准的作者现在非常后悔。如果您尝试用auto 替换难看的显式类型名称并想知道为什么编译器拒绝该代码,您会发现问题。 @EOF 我想你说的是vector&lt;bool&gt;::reference slot_ref = slots[i];?如果我将其更改为自动它仍然为我编译...? 但是如果你把它改成auto slot_ref = slots[i];,你就改变了意思,因为slot_ref不是一个引用而是一个副本。 【参考方案1】:

std::vector&lt;bool&gt; 被有效地实现为动态位集。执行此操作的确切方式未在标准中指定,但将实现为某个整数类型的连续序列(例如std::uint32_t),其中每个位代表vector 中的bool 值。

因此,将任何一个值设置为true false 实际上是在修改更大对象的一部分:内部整数对象。您不能简单地锁定正在修改的单个位并期望线程安全,因为可能有多个线程同时改变该整数。

正确同步的唯一方法是不使用vector&lt;bool&gt;,而是使用std::vector&lt;std::uint8_t&gt; 之类的东西,其中每个uint8_t 都是可以独立锁定和修改的唯一对象。


作为一个具体的例子,假设vector&lt;bool&gt; 中使用的底层 int 是 uint32_t -- 这样您就可以一次在 32 位上进行操作。

现在想象一下,通过这三个操作来设置一个位:

    将整数值加载到寄存器中 执行 OR 操作将位设置为1,并且 将整数值存储回内存

如果整数从值0x0(所有位设置为零)开始,并且我们有两个线程试图同时设置位01,那么不会 em> 是互斥锁的任何锁定,因为每个位都有一个单独的互斥锁。相反,我们可能会看到以下竞争条件:

 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。从技术上讲,如果行为未定义,一切皆有可能。

【讨论】:

哇,完美的答案!非常感谢...老实说,这是一个非常愚蠢的优化。作为用户,我希望向量的每个元素都是单独的类型... vector&lt;bool&gt; 专业化绝对是标准库中最具争议和令人遗憾的设计决策之一。不幸的是,它不太可能真正改变太多,以避免破坏向后兼容性......顺便说一句,如果这回答了您的问题,请确保将其标记为解决方案! :)【参考方案2】:

只使用一个互斥锁和一个condition variable 怎么样?请查看 cppreference 中的示例并考虑重新设计您的解决方案。

【讨论】:

如前所述,我的目标不是实现另一个正确的实现,而是理解为什么这不是线程安全的

以上是关于在 C++ 中使用插槽和互斥体向量的线程的互斥插槽分配器的主要内容,如果未能解决你的问题,请参考以下文章

C++多线程同步技巧--- 互斥体

多个线程试图锁定

使用互斥锁锁定向量 - Boost

用户模式同步之互斥体小解

线程资源同步——互斥量和条件变量

从 boost::signals2 安全断开