一位读者一位作家。关于互斥体和原子内置函数的一些常见问题

时间:2011-08-16 15:59:25

标签: c++ multithreading mutex

我有一个父级和一个工作线程共享一个bool标志和一个std :: vector。父母只读(即读取bool或调用my_vector.empty());工人只写。

我的问题:

  • 我是否需要互斥保护bool标志?

  • 我可以说所有bool读/写都是固有的原子操作吗?如果您说“是”或“否”,您从哪里获取信息?

  • 我最近听说过GCC Atomic-builtin。我可以使用这些来使我的标志读/写原子而不必使用互斥锁吗?有什么不同?我理解Atomic builtins归结为机器代码,但即使是互斥体也归结为CPU的内存屏障指令吧?为什么人们将互斥锁称为“操作系统级别”构造?

  • 我是否需要互斥保护我的std :: vector?回想一下,工作线程填充这个向量,而父对象只调用它上面的empty()(即只读取它)

  • 我不相信bool或vector都需要互斥保护。我合理化如下,“好吧,如果我在更新之前读取共享内存...那仍然没问题,我将在下一次获得更新的值。更重要的是,我不明白为什么写入者应该被阻止阅读正在阅读,因为毕竟,读者只是在阅读!“

如果有人能指出我正确的方向,那就太棒了。我在GCC 4.3上,而Intel x86是32位。 非常感谢!

4 个答案:

答案 0 :(得分:13)

  

我是否需要互斥保护bool标志?

不一定,原子指令会这样做。到atomic instruction我的意思是编译器内部函数,a)防止编译器重新排序/优化,b)导致原子读/写,c)发出适当的内存栅栏,以确保CPU之间的可见性(当前x86 CPU不需要)雇用MESI cache coherency protocol)。与gcc atomic builtins类似。

  

我可以说所有bool读/写都是固有的原子操作吗?如果您说“是”或“否”,您从何处获取信息?

取决于CPU。对于英特尔CPU - 是的。请参阅Intel® 64 and IA-32 Architectures Software Developer's Manuals

  

我最近听说过GCC Atomic-builtin。我可以使用这些来使我的标志读/写原子而不必使用互斥锁吗?有什么不同?我理解Atomic builtins归结为机器代码,但即使是互斥体也归结为CPU的内存屏障指令吧?为什么人们将互斥锁称为“操作系统级别”构造?

原子与互斥之间的区别在于后者可以让等待线程进入睡眠状态,直到释放互斥锁。使用原子,你只能忙着旋转。

  

我需要互斥保护我的std :: vector吗?回想一下,工作线程填充这个向量,而父对象只调用它上面的empty()(即只读取它)

你做。

  

我认为bool或vector都不需要互斥保护。我合理化如下,“好吧,如果我在更新之前读取共享内存...那仍然没问题,我将在下一次获得更新的值。更重要的是,我不明白为什么写入者应该被阻止阅读正在阅读,因为毕竟读者只是在阅读!“

根据实现,vector.empty()可能涉及读取两个缓冲区开始/结束指针并减去或比较它们,因此有可能您读取一个指针的新版本和另一个指针的旧版本一个互斥体。可能会出现令人惊讶的行为。

答案 1 :(得分:2)

数目:

  1. 您需要保护可能同时由两个或多个线程操作的bool(或任何其他变量)。您可以使用互斥锁或通过原子操作bool来执行此操作。
  2. Bool读取并且bool写入可能是原子操作,但是两个顺序操作肯定不是(例如,读取然后写入)。稍后会详细介绍。
  3. Atomic builtins提供了上述问题的解决方案:能够在一个不能被另一个线程中断的步骤中读取和写入变量。这使得操作成为原子。
  4. 如果你使用bool标志作为'mutex'(也就是说,只有将bool标志设置为true的线程才有权修改向量)那么你就可以了。互斥是由布尔值管理的,只要你使用原子操作修改bool就应该全部设置。
  5. 要回答这个问题,让我举一个例子:
  6. bool              flag(false);
    std::vector<char> my_vector;
    
    while (true)
    {
        if (flag == false) // check to see if the mutex is owned
        {
            flag = true; // obtain ownership of the flag (the mutex)
    
            // manipulate the vector
    
            flag = false; // release ownership of the flag
        }
    }
    

    在多线程环境中的上述代码中,线程可能在if语句(读取)和赋值(写入)之间被抢占,这意味着两个< / em>(或更多)具有此类代码的线程同时“拥有”互斥锁(以及向量的权限)。这就是原子操作至关重要的原因:它们确保在上述情况下,标志一次只能由一个线程设置,因此确保向量一次只能由一个线程操作。

    请注意,将标志设置为false不一定是原子操作,因为此实例是唯一有权修改它的实例。

    粗略(读取:未经测试)解决方案可能类似于:

    bool              flag(false);
    std::vector<char> my_vector;
    
    while (true)
    {
        // check to see if the mutex is owned and obtain ownership if possible
        if (__sync_bool_compare_and_swap(&flag, false, true)) 
        {
            // manipulate the vector
    
            flag = false; // release ownership of the flag
        }
    }
    

    原子内置文件的文档为:

      

    如果比较成功并且写入了newval,则“bool”版本返回true。

    这意味着操作将检查标志是否为false以及是否将值设置为true。如果值为false,则返回true,否则返回false。所有这些都发生在原子步骤中,因此保证不会被另一个线程抢占。

答案 2 :(得分:2)

从C ++ 11标准的角度来看,您必须使用互斥锁保护bool,或者使用std::atomic<bool>。即使您确定无论如何都要以原子方式读取和写入bool,编译器仍有可能优化对它的访问,因为它不知道可能访问它的其他线程。

如果由于某种原因您绝对需要平台的最新性能,请考虑阅读“英特尔64和IA-32架构软件开发人员手册”,它将告诉您架构的工作原理。但是,当然,这将使您的程序无法移植。

答案 3 :(得分:0)

我没有足够的专业知识来回答你的整个问题,但是在默认情况下读取是非原子的情况下,你的最后一颗子弹是不正确的。

上下文切换可以在任何地方发生,读者可以通过读取中途切换上下文,编写器可以切换并执行完整写入,然后读者将完成读取。读者既不会看到第一个值,也不会看到第二个值,但可能会看到一些非常不准确的中间值。