在这个多线程 C++ 代码中是不是需要“易失性”?

Posted

技术标签:

【中文标题】在这个多线程 C++ 代码中是不是需要“易失性”?【英文标题】:Is 'volatile' needed in this multi-threaded C++ code?在这个多线程 C++ 代码中是否需要“易失性”? 【发布时间】:2010-08-31 19:31:15 【问题描述】:

我用 C++ 编写了一个 Windows 程序,它有时使用两个线程:一个用于执行耗时工作的后台线程;和另一个管理图形界面的线程。这样程序仍然可以响应用户,这是能够中止某个操作所必需的。线程通过共享的bool 变量进行通信,当GUI 线程向工作线程发出中止信号时,该变量设置为true。这是实现这种行为的代码(我已经去掉了不相关的部分):

GUI线程执行的代码


class ProgressBarDialog : protected Dialog 

    /**
     * This points to the variable which the worker thread reads to check if it
     * should abort or not.
     */
    bool volatile* threadParameterAbort_;

    ...

    BOOL CALLBACK ProgressBarDialog::DialogProc( HWND dialog, UINT message, 
        WPARAM wParam, LPARAM lParam ) 

        switch( message ) 
            case WM_COMMAND :
                switch ( LOWORD( wParam ) ) 

                    ...

                    case IDCANCEL :
                    case IDC_BUTTON_CANCEL :
                        switch ( progressMode_ ) 
                            if ( confirmAbort() ) 
                                // This causes the worker thread to be aborted
                                *threadParameterAbort_ = true;
                            
                            break;
                        

                        return TRUE;
                
        

        return FALSE;
    

    ...

;

工作线程执行的代码


class CsvFileHandler 

    /**
     * This points to the variable which is set by the GUI thread when this
     * thread should abort its execution.
     */
    bool volatile* threadParamAbort_;

    ...

    ParseResult parseFile( ItemList* list ) 
        ParseResult result;

        ...

        while ( readLine( &line ) ) 
            if ( ( threadParamAbort_ != NULL ) && *threadParamAbort_ ) 
                break;
            

            ...
        

        return result;
    

    ...

;

两个线程中的threadParameterAbort_ 指向在结构中声明的bool 变量,该结构在创建时传递给工作线程。它被声明为

bool volatile abortExecution_;

我的问题是:这里是否需要使用volatile,上面的代码是否足以确保程序是线程安全的?我在这里证明使用volatile 的理由(有关背景请参阅this question)的理由是:

防止读取*threadParameterAbort_以使用缓存,而是从内存中获取值,并且

防止编译器因优化而移除工作线程中的if子句。

(以下段落仅涉及程序本身的线程安全性,,我重复一遍,涉及声称volatile in任何方式都提供了确保线程安全的任何方法。)据我所知,它应该是线程安全的,因为 bool 变量的设置在大多数(如果不是全部)架构中应该是原子操作。但我可能是错的。而且我还担心编译器是否会重新排序指令,例如破坏线程安全。但最好是安全(不是双关语)而不是抱歉。

编辑: 我措辞中的一个小错误使这个问题看起来好像我在问volatile 是否足以确保线程安全。这不是我的意图——volatile 确实不能以任何方式确保线程安全——但我的意思是,上面提供的代码是否表现出正确的行为以确保程序是线程安全的。

【问题讨论】:

复制更好的half of that。最近的 IIRC 是 the question。 昨天来自同一个人的问题涉及相同的领域:***.com/questions/3604569/…。如果您不编写设备驱动程序并且对编译器不是很熟悉,则不需要volatile @Potatoswatter:但在编写多线程代码时,volatile 确实很有用,如本文(drdobbs.com/cpp/184403766)所示,该文章已在其中一个答案中链接。 那篇文章已经过时了。别介意。 volatile 可能有助于解决线程库不足的问题,但这不再是问题。 (请记住,C++ 根本不是为多线程设计的,所以如果像 volatile 这样的语言特性有助于多线程,那纯属巧合。)考虑到另一个问题中讨论的问题,它不足以实现任何同步刮。正如杰瑞所说,既没有必要也没有充分。永远不要使用它。熟悉您的线程库并使用干净、受支持的同步。 @Potatoswatter:啊。非常感谢您指出这一点。伙计,我希望我能早点找到 ***——它充满了有用的信息和人员。 ^^ 再次感谢。 【参考方案1】:

你不应该依赖 volatile 来保证线程安全,这是因为即使编译器会保证变量总是从内存中读取(而不是寄存器缓存),在多处理器环境中,内存屏障也会是必需的。

而是在共享内存周围使用正确的锁。像临界区这样的锁通常是非常轻量级的,并且在没有争用的情况下可能会全部在用户端实现。它们还将包含必要的内存屏障。

Volatile 只能用于内存映射 IO,其中多次读取可能会返回不同的值。对于内存映射写入也是如此。

【讨论】:

但是如果我删除volatile,编译器会错误地优化掉if 子句吗?或者*threadParamAbort_ 的值可能总是使用缓存?通过互斥锁或信号量应用临界区是否确保不会发生这种情况? 互斥量或信号量是非纯函数。它可能有副作用(更改全局变量),因此编译器可能不会假设 *threadParamAbort_ 的值将保持不变。并且必须在互斥锁或临界区锁定或发出信号后重新读取。而且由于一次只有 1 个线程可以保存一个临界区,因此在锁内重新排序指令没有任何问题。只要确保你正确地应用了锁。 好的,所以通过用临界区包装if 子句将阻止编译器对其进行优化,对吗?即使threadParameterAbort_ 实际上不是全局变量?如果是这样,则不需要volatile,因为这是由关键部分提供的。并同意,在关键部分重新排序并没有错。但是,我是否需要将threadParameterAbort_ 的设置包装在带有关键部分的 GUI 线程中?似乎是多余的,因为该操作本身是原子的,是吗? 我认为 threadParameterAbort_ 是一个指向共享变量的指针,因此该值实际上是全局的。是的,即使某些东西看起来是全局的,也要使用关键部分。它们非常便宜,可以确保您不受处理器读/写重新排序的影响。 @deus-ex-machina399:即使读取和写入布尔值是原子操作?【参考方案2】:

Wikipedia 说得很好。

在 C 和 C++ 中, volatile 关键字旨在允许访问内存映射设备 允许在 setjmp 之间使用变量 允许在信号处理程序中使用 sig_atomic_t 变量

对 volatile 变量的操作是 不是原子的,也不是建立适当的 发生之前的关系 穿线。这是根据 相关标准(C、C++、POSIX、 WIN32),这是事实 对于当前绝大多数 实施。 易变的 关键字基本上没有价值作为 便携式线程构造。

【讨论】:

我不明白为什么人们一直认为volatile 提供了任何类型的线程安全。 +1 它们来自 Java,即使在那里也没有线程安全保证。 ***是一个糟糕的引用来源(即使是***的创始人也说不要使用***作为引用来源)。将其作为研究的起点,但至少引用权威来源。 为什么,另一种选择是什么都不引用。或者我可以引用 C 或 C++ 文档,这会更难理解。此外,它并不完全像我在这里提交论文。如果有人在***上简洁地回答它,我将链接到***。我根本不理解对下意识的 Wikipedia 仇恨,也不理解这些任意的发布规则,另一种选择是像大多数答案一样简单地不引用来源。 @gablin:不,你误解了我的意思。 volatile 与线程安全无关。完全没有。【参考方案3】:

volatile 对于 C++ 中的多线程来说既非必要也不充分。它禁用了完全可以接受的优化,但无法强制执行所需的原子性等事情。

编辑:我可能会使用InterlockedIncrement,而不是使用临界区,这样可以减少开销的原子写入。

然而,我通常做的是连接一个线程安全队列(或双端队列)作为线程的输入。当你有事情要线程做时,你只需将描述作业的数据包放入队列中,线程会在可能的时候做。当您希望线程正常关闭时,您将“关闭”数据包放入队列中。如果您需要立即中止,请改用双端队列,并将“中止”命令放在双端队列的前面。从理论上讲,它的缺点是它在完成当前任务之前不会中止线程。这意味着您希望将每个任务的大小/延迟范围保持在与您当前检查标志的频率大致相同的范围内。

这种通用设计避免了一大堆 IPC 问题。

【讨论】:

好的;你将不得不扩展它。我同意这还不够。我同意对 volatile 的可移植性依赖不会有用。但是它不会在使用 Cl1 的 Windows 上提供一些好处吗,因为内存没有被缓存,因此我们不需要担心跨多个线程的缓存一致性问题。 @Jerry Coffin:我很清楚volatile 绝不保证线程安全或原子性。另外,写入或读取bool 值不是原子操作吗? @Martin: volatile 只阻止编译器将数据保存在寄存器中;它没有来防止/帮助解决缓存一致性问题。 @gablin:读或写bool 可能是原子的——再说一次,它可能不是。它可能通常默认使用 MS VC++,但绝对有可能创建一个不会被原子读取/写入的bool @Jerry Coffin:好的。那么当我们有多个线程(可能在不同的处理器上)时,我们如何处理缓存一致性?所有现代处理器都在硬件级别处理这个问题吗?还是有别的? @gablin:如果仅此而已,我会同意。 OTOH,我倾向于从一开始就设计它。大多数程序会随着时间的推移而增长,并且拥有它通常会使这种增长更易于管理。【参考方案4】:

关于我对昨天问题的回答,不,volatile 是不必要的。其实这里的多线程是无关紧要的。

    while ( readLine( &line ) )  // threadParamAbort_ is not local:
        if ( ( threadParamAbort_ != NULL ) && *threadParamAbort_ ) 
    阻止读取 *threadParameterAbort_ 以使用缓存,而是从 记忆,以及 防止编译器删除worker中的if子句 线程由于优化。

函数readLine是外部库代码,否则调用外部库代码。因此,编译器不能假设有任何它没有修改的非局部变量。一旦形成了指向对象(或其超对象)的指针,就可以将其传递并存储在任何地方。编译器无法跟踪哪些指针最终会出现在全局变量中,哪些不会。

因此,编译器假定readLine 有自己的私有static bool *threadParamAbort_,并修改该值。因此需要从内存中重新加载。

【讨论】:

我明白了。感谢您澄清这一点。【参考方案5】:

似乎在这里描述了相同的用例:volatile - Multithreaded Programmer's Best Friend by Alexandrescu。它指出,在这种情况下(创建标志)volatile 可以完美使用。

所以,是的正是在这种情况下代码应该是正确的。 volative 将阻止 - 从缓存中读取并阻止编译器优化 if 语句。

【讨论】:

【参考方案6】:

虽然其他答案是正确的,但我也建议您查看MSDN documentation for volatile 的“Microsoft 特定”部分

【讨论】:

它确实看起来与我在程序中应用的非常相似。但是我还应该使用临界区吗?我的程序是否需要它? 无需回复(查看其他答案之一的讨论)。【参考方案7】:

我认为它可以正常工作(无论是否是原子的),因为您只是使用它来取消后台操作。

volatile 要做的主要事情是防止变量被缓存或从寄存器中检索。您可以确定它来自主内存。

所以如果我是你,是的,我会将变量定义为 volatile。

我认为最好假设需要 volatile 以确保变量在写入然后由其他线程读取时它们实际上尽快获得该变量的正确值并确保 IF 没有被优化掉(虽然我怀疑它会是)。

【讨论】:

如果读取和写入threadParamAbort_ 确实不是原子的,那么我也需要在单个 CPU 系统上担心它。正如我也向大家指出的那样,我知道 volatile 不为任何操作提供原子性;那么我们可以不要再向我指出这一点了吗? 静下心来听。我最初是在 2-3 天前写的,所以不需要对我说什么。我的不是新答案好吗?原子性与线程安全不同但与线程安全有关,这就是您在问题中提到的所有内容。现在,在更多地考虑了您的需求后,我更新了我的回复。【参考方案8】:

好的,所以你在 volatile 和线程安全方面已经受够了!但是......

您的特定代码的一个示例(尽管在您的控制范围内)是您在一个“事务”中多次查看变量的事实:

if ( ( threadParamAbort_ != NULL ) && *threadParamAbort_ )

如果由于某种原因 threadParamAbort_ 在左侧之后和右侧之前被删除,那么您将取消引用已删除的指针。同样,鉴于您可以控制,这不太可能,但这是 volatile 和 atomicity 无法为您做的一个示例。

【讨论】:

是的,这是一个正确的观察,但我知道在执行这行代码时threadParamAbort_ 永远不会被删除,因为我的代码是这样构造的。当然,除非存在错误,但这是一个完全不同的问题。

以上是关于在这个多线程 C++ 代码中是不是需要“易失性”?的主要内容,如果未能解决你的问题,请参考以下文章

C++ 易失性成员函数

同步对象是不是可缓存?

具有易失性和外部数据访问的 C++ 常量正确性

多线程中的Java volatile关键字

C# 内存模型和非易失性变量在其他线程创建之前初始化

嵌入式 C - 易失性限定符在我的中断例程中无关紧要