当另一个线程可以设置它时(最多一次),是否可以读取共享布尔标志而不锁定它?

Joh*_*ohn 42 c++ multithreading boolean locking monitor

我希望我的线程更优雅地关闭,所以我试图实现一个简单的信令机制.我不认为我想要一个完全事件驱动的线程,所以我有一个工人用一个方法来使用一个关键部分Monitor(相当于一个C#lock我相信)来优雅地停止它:

DrawingThread.h

class DrawingThread {
    bool stopRequested;
    Runtime::Monitor CSMonitor;
    CPInfo *pPInfo;
    //More..
}
Run Code Online (Sandbox Code Playgroud)

DrawingThread.cpp

void DrawingThread::Run() {
    if (!stopRequested)
        //Time consuming call#1
    if (!stopRequested) {
        CSMonitor.Enter();
        pPInfo = new CPInfo(/**/);
        //Not time consuming but pPInfo must either be null or constructed. 
        CSMonitor.Exit();
    }
    if (!stopRequested) {
        pPInfo->foobar(/**/);//Time consuming and can be signalled
    }
    if (!stopRequested) {
        //One more optional but time consuming call.
    }
}


void DrawingThread::RequestStop() {
    CSMonitor.Enter();
    stopRequested = true;
    if (pPInfo) pPInfo->RequestStop();
    CSMonitor.Exit();
}
Run Code Online (Sandbox Code Playgroud)

我理解(至少在Windows中)Monitor/ locks是最便宜的线程同步原语,但我很想避免过度使用.我应该包装每个读取此布尔标志吗?它被初始化为false,并且只在请求停止时设置为true(如果在任务完成之前请求它).

我的导师建议保护甚至bool是因为读/写可能不是原子的.我认为这一次射击旗是证明规则的例外吗?

Die*_*ühl 45

在没有同步的情况下读取可能在不同线程中修改的内容永远不可能.需要什么级别的同步取决于您实际阅读的内容.对于原始类型,您应该查看原子读取,例如以形式std::atomic<bool>.

始终需要同步的原因是处理器将具有可能在高速缓存行中共享的数据.如果没有同步,则没有理由将此值更新为可能在其他线程中更改的值.更糟糕的是,如果没有同步,如果存储在值附近的内容被更改和同步,则可能会写入错误的值.

  • @Tudor:我和一些人谈过,根据英特尔人的说法,你可以通过`volatile`读取(它需要是`volatile`以防止编译器做一些有趣的事情)以及在Intel x86系统上正常写入.这不一定扩展到其他x86系统,似乎它没有扩展到多插槽AMD x86系统.结果是你需要非常准确地知道你运行的是什么x86系统才能知道它是否可靠运行. (2认同)

Tud*_*dor 12

布尔赋值是原子的.那不是问题.

问题是由于编译器或CPU指令重新排序或数据缓存,线程可能看不到由不同线程完成的变量的更改(即读取布尔标志的线程可能读取缓存值,而不是实际更新值).

解决方案是一个内存栅栏,它实际上是由锁定语句隐式添加的,但是对于单个变量来说它是过度的.只需声明为std::atomic<bool>.

  • 注意,在C++中声明一些`volatile`对**同步**没有**影响**!使用`volatile`是为了防止它省略和/或重新排序指令(这与Java中的`volatile`的语义不同).您还需要告诉CPU需要同步.您需要使用相应的指令,例如`std :: atomic <bool>`. (20认同)
  • "布尔赋值是原子的":只是出于兴趣,这是C或C++标准所要求的吗? (2认同)
  • @NiklasB.我认为这意味着:你不能最终得到线程看到的"bool"状态不一致.在这个意义上它确实是原子的.但是,这并不意味着该值以任何方式与不同的内核同步.要使这些成为原子,你需要同步,例如使用`std :: atomic <bool>`. (2认同)
  • @Niklas B.:通过原子,我的意思是该值不会遭受"撕裂",例如32位机器上的长时间可能会被分成两半,因为它需要两个寄存器副本.当然,这并不意味着可见性. (2认同)

Max*_*ert 7

我相信答案是"这取决于".如果你正在使用C++ 03,标准中没有定义线程,你必须阅读你的编译器和你的线程库所说的内容,尽管这种事情通常被称为"良性竞赛" ,并且通常没关系.

如果您使用的是C++ 11,那么良性竞赛是未定义的行为.即使未定义的行为对底层数据类型没有意义.问题是编译器可以假定程序没有未定义的行为,并根据它进行优化(另请参见从那里链接的第1部分和第2部分).例如,您的编译器可能决定一次读取标志并缓存该值,因为它是未定义的行为,在没有某种互斥或内存屏障的情况下写入另一个线程中的变量.

当然,很可能你的编译器承诺不进行优化.你需要看看.

最简单的解决方案是std::atomic<bool>在C++ 11中使用,或者在其他地方使用Hans Boehm的atomic_ops.

  • 它不"依赖":如果你需要跨多个线程共享可变数据,即使它只是一个`bool`,你需要某种同步.本质上,这是为了通知CPU安排当前正在缓存的对象变得可见. (2认同)
  • 这取决于(1)即使标准说某事未定义,也允许实现**定义将要发生的事情,良性竞争是足够普遍的,以至于编译器可能会这样做; (2)虽然良性竞赛在C++ 11中肯定是未定义的,但对于C++ 03来说更难说(尽管我链接到的第一篇论文确切地说明了这一点,当然它也表明了这些类型的数据竞争的广泛性是). (2认同)