一位读者.一个作家.关于互斥锁和原子构造的一些一般问题

One reader. One writer. Some general questions about mutexes and atomic-builtins

本文关键字:问题 一位 于互斥 作家 一个      更新时间:2023-10-16

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

我的问题:

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

  • 我能说所有bool读/写本质上是原子操作吗?如果你回答"是"或"不是",你的信息是从哪里来的?

  • 我最近听说GCC原子构建。我可以使用这些,使我的标志读/写原子,而不必使用互斥锁?有什么区别呢?我知道原子内置可以归结为机器代码,但即使是互斥也可以归结为CPU的内存屏障指令,对吗?为什么人们称互斥锁为"操作系统级"结构?

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

  • 我不认为互斥锁保护是必要的,无论是bool还是vector。我的解释如下:"好吧,如果我在共享内存更新之前读取它……这仍然很好,下次我将得到更新的值。更重要的是,我不明白为什么要在读者阅读的时候阻止作者,因为毕竟,读者只是在阅读!"

如果有人能给我指个方向,那就太好了。我使用的是GCC 4.3和Intel x86 32位。非常感谢!

我需要互斥保护bool标志吗?

不一定,原子指令就可以了。通过atomic instruction,我指的是编译器的内在功能,a)防止编译器重新排序/优化,b)导致原子读/写,c)发出适当的内存栅栏,以确保cpu之间的可见性(对于当前使用MESI缓存一致性协议的x86 cpu来说没有必要)。类似于gcc的原子构建。

我能说所有bool读/写本质上是原子操作吗?如果你回答"是"或"不是",你的信息是从哪里来的?

取决于CPU。对于英特尔的cpu——是的。参见Intel®64和IA-32架构软件开发人员手册。

我最近听说了GCC原子构建。我可以使用这些,使我的标志读/写原子,而不必使用互斥锁?有什么区别呢?我知道原子内置可以归结为机器代码,但即使是互斥也可以归结为CPU的内存屏障指令,对吗?为什么人们称互斥锁为"操作系统级"结构?

原子和互斥锁的区别在于,后者可以让等待的线程进入睡眠状态,直到互斥锁被释放。对于原子,你只能自旋。

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

我不认为互斥保护对bool型和vector型都是必要的。我的解释如下:"好吧,如果我在共享内存更新之前读取它……这仍然很好,下次我将得到更新的值。更重要的是,我不明白为什么要在读者阅读的时候阻止作者,因为毕竟,读者只是在阅读!"

根据实现的不同,vector.empty()可能涉及读取两个缓冲区开始/结束指针,并减去或比较它们,因此有可能在没有互斥锁的情况下读取一个指针的新版本和另一个指针的旧版本。令人惊讶的行为可能随之而来。

从c++ 11标准的角度来看,你必须用互斥锁来保护bool值,或者使用std::atomic<bool>。即使你确定bool是自动读写的,编译器仍然有可能优化掉对它的访问,因为它不知道其他线程可能访问它。

如果出于某种原因,你绝对需要平台的最新性能,请考虑阅读"Intel 64和IA-32架构软件开发人员手册",它将告诉你如何在架构下工作。但是,这当然会使你的程序不可移植。

答案:

  1. 您需要保护bool(或任何其他变量),它有可能同时被两个或多个线程操作。你既可以使用互斥锁,也可以自动操作bool。
  2. Bool读和Bool写可能是原子操作,但两个顺序操作肯定不是原子操作(例如,先读后写)。
  3. 原子内置为上面的问题提供了一个解决方案:在一个不能被另一个线程中断的步骤中读写变量的能力。这使得操作变得原子化。
  4. 如果你正在使用bool标志作为你的'互斥'(也就是说,只有将bool标志设置为true的线程才有权修改矢量),那么你就可以了。互斥是由布尔值管理的,只要你使用原子操作修改bool值,你应该都设置好了。
  5. 为了回答这个问题,让我举一个例子:

,

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语句(读)和赋值(写)之间被抢占,这意味着两个(或更多)具有这种代码的线程可能同时"拥有"互斥锁(和对向量的权利)。这就是原子操作至关重要的原因:它们确保在上述场景中,每次只能由一个线程设置标志,从而确保vector每次只能由一个线程操作。

注意,将标志设置回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。所有这些都发生在原子步骤中,因此保证不会被另一个线程抢占。

我没有专业知识来回答你的整个问题,但是你的最后一项是不正确的,因为读取默认是非原子的。

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