【问题标题】:Mutually exclusive slot allocator for threads using a vector of slots and mutexes in C++在 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不是一个引用而是一个副本。

标签: c++ multithreading mutex


【解决方案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 位上进行操作。

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

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

如果整数从值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 中的示例并考虑重新设计您的解决方案。

【讨论】:

  • 如前所述,我的目标不是实现另一个正确的实现,而是理解为什么这不是线程安全的
猜你喜欢
  • 1970-01-01
  • 2023-02-06
  • 1970-01-01
  • 1970-01-01
  • 2016-12-27
  • 2013-10-08
  • 1970-01-01
  • 1970-01-01
  • 2013-01-31
相关资源
最近更新 更多