首页 文章

C volatile关键字是否引入了内存栅栏?

提问于
浏览
77

我知道 volatile 通知编译器可能会更改该值,但为了完成此功能,编译器是否需要引入内存栅栏才能使其工作?

根据我的理解,易失性对象的操作顺序不能重新排序,必须保留 . 这似乎暗示一些内存栅栏是必要的,并且没有真正解决这个问题的办法 . 我说的是对的吗?


this related question有一个有趣的讨论

Jonathan Wakely writes

...对于不同的volatile变量的访问不能由编译器重新排序,只要它们出现在单独的完整表达式中......对于线程安全而言volatile是无用的,但不是由于他给出的原因 . 这不是因为编译器可能会重新排序对易失性对象的访问,而是因为CPU可能会重新排序它们 . 原子操作和内存屏障阻止编译器和CPU重新排序

David Schwartz回复in the comments

...从C标准的角度来看,编译器执行某些操作与发出导致硬件执行某些操作的指令的编译器之间没有区别 . 如果CPU可以重新排序对volatiles的访问,则标准不要求保留其订单 . ...... C标准对重新排序的内容没有任何区别 . 并且你不能争辩说CPU可以重新排序它们没有可观察到的效果,所以没关系--C标准将它们的顺序定义为可观察的 . 如果编译器生成的代码使平台符合标准要求,则编译器符合平台上的C标准 . 如果标准要求对挥发物的访问不能重新排序,则重新排序它们的平台不符合要求 . ...我的观点是,如果C标准禁止编译器重新排序对不同易失性的访问,理论上这种访问的顺序是程序可观察行为的一部分,那么它还要求编译器发出禁止CPU这样做 . 该标准没有区分编译器的功能以及编译器生成的代码使CPU执行的操作 .

这确实产生两个问题:它们中的任何一个是“正确的”吗?实际的实现到底做了什么?

13 回答

  • 5

    而不是解释 volatile 的作用,请允许我解释何时应该使用 volatile .

    • 在信号处理程序内部时 . 因为写入 volatile 变量几乎是标准允许您在信号处理程序中执行的唯一操作 . 从C 11开始,您可以使用 std::atomic 来实现此目的,但前提是原子是无锁的 .

    • 处理 setjmp according to Intel时 .

    • 直接处理硬件时,您希望确保编译器不会优化您的读取或写入 .

    例如:

    volatile int *foo = some_memory_mapped_device;
    while (*foo)
        ; // wait until *foo turns false
    

    如果没有 volatile 说明符,则允许编译器完全优化循环 . volatile 说明符告诉编译器它可能不会假设2个后续读取返回相同的值 .

    请注意, volatile 与线程无关 . 如果有一个不同的线程写入 *foo ,则上述示例不起作用,因为不涉及获取操作 .

    在所有其他情况下, volatile 的使用应被视为不可移植,并且不再通过代码审查,除非处理前C 11编译器和编译器扩展(例如msvc的 /volatile:ms 开关,默认情况下在X86 / I64下启用) .

  • 6

    C volatile关键字是否引入了内存栅栏?

    符合规范的C编译器不需要引入内存栅栏 . 您的特定编译器可能;将您的问题提交给编译器的作者 .

    C中"volatile"的功能与线程无关 . 请记住,"volatile"的目的是禁用编译器优化,以便从因外部条件而变化的寄存器中读取数据不会被优化掉 . 是否由不同CPU上的不同线程写入的内存地址是由于外部条件而发生变化的寄存器?不 . 再次,如果一些编译器作者选择处理由不同CPU上的不同线程写入的内存地址,就好像它们是由于外部条件而改变的寄存器,那就是他们的业务;他们不需要这样做 . 它们也不是必需的 - 即使它确实引入了内存栅栏 - 例如,确保每个线程都能看到易失性读写的一致排序 .

    事实上,volatile在C / C中的线程化几乎没用 . 最佳做法是避免它 .

    此外:内存防护是特定处理器体系结构的实现细节 . 在C#中,volatile显式设计用于多线程,规范并没有说将引入半个fences,因为程序可能正在运行没有_846479的实现细节的架构将来可能会发生变化 .

    您关心任何语言中与多线程有关的volatile的语义这一事实表明您正在考虑跨线程共享内存 . 考虑一下就不这样做 . 它使您的程序更难理解,更有可能包含微妙的,不可能重现的错误 .

  • 0

    David忽略的事实是,c标准指定了几个线程在特定情况下交互的行为,而其他所有线程都会导致未定义的行为 . 如果不使用原子变量,则不确定涉及至少一次写入的竞争条件 .

    因此,编译器完全有权放弃任何同步指令,因为您的cpu只会注意到由于缺少同步而出现未定义行为的程序中的差异 .

  • 2

    首先,C标准不保证正确排序非原子读/写所需的内存障碍 . 建议将 volatile 变量用于MMIO,信号处理等 . 在大多数实现中, volatile 对多线程无用,通常不推荐使用.846481_ .

    关于volatile访问的实现,这是编译器的选择 .

    描述 gcc 行为的 article 表明您不能使用易失性对象作为内存屏障来对易失性存储器进行一系列写入 .

    关于 icc 行为,我发现 source 也告诉volatile不保证订购内存访问 .

    Microsoft VS2013 编译器有不同的行为 . 这个 documentation 解释了volatile如何强制执行Release / Acquire语义,并允许在多线程应用程序的锁定/发布中使用volatile对象 .

    需要考虑的另一个方面是相同的编译器可能具有 different behavior wrt. to volatile depending on the targeted hardware architecture . 关于MSVS 2013编译器的这个 post 清楚地说明了针对ARM平台使用volatile进行编译的细节 .

    所以我的答案是:

    C volatile关键字是否引入了内存栅栏?

    将是:不保证,可能不是,但一些编译器可能会这样做 . 你不应该依赖它的事实 .

  • 47

    据我所知,编译器只在Itanium体系结构上插入一个内存栅栏 .

    volatile 关键字最适合用于异步更改,例如信号处理程序和内存映射寄存器;它通常是用于多线程编程的错误工具 .

  • 12

    这取决于“编译器”是哪个编译器 . 从2005年开始,Visual C就可以了 . 但是标准并没有要求它,所以其他一些编译器则不需要它 .

  • 12

    这主要来自内存,基于前C 11,没有线程 . 但是参与了关于委托中线程的讨论,我可以说委员会从来没有意图 volatile 可以用于线程之间的同步 . 微软提出了这个建议,但提案没有提出 .

    volatile 的关键规范是访问volatile表示"observable behavior",就像IO一样 . 以同样的方式,编译器无法重新排序或删除特定的IO,它无法重新排序或删除对volatile对象的访问(或者更准确地说,通过具有volatile限定类型的左值表达式进行访问) . 事实上,volatile的最初意图是支持内存映射IO . 然而,"problem"与它是实现定义的构成"volatile access" . 许多编译器实现它,好像定义是"an instruction which reads or writes to memory has been executed" . 如果实现指定了,那么这是一个合法的,尽管是无用的定义 . (我还没有找到任何编译器的实际规范 . )

    可以说(至少在内存映射IO上使用volatile,至少在Sparc或Intel架构上使用volatile . 从来没有,我看过的任何编译器(Sun CC,g和MSC)都没有输出任何fence或membar指令(大约在微软提出扩展 volatile 规则的时候,我认为他们的一些编译器实现了他们的提议,并且确实发出了针对易失性访问的栅栏指令 . 如果它依赖于某些编译器选项,我会感到惊讶 . 我检查的版本 - 我认为这是VS6.0 - 然而没有发射围栏 . )

  • 0

    它不必 . 易失性不是同步原语 . 它只是禁用优化,即您在一个线程中获得可预测的读写序列,其顺序与抽象机器规定的顺序相同 . 但是,在不同的线程中,读取和写入首先没有顺序,所以说保留或不保留它们的顺序是没有意义的 . 可以通过同步原语 Build theads之间的顺序,在没有它们的情况下获得UB .

    关于记忆障碍的一些解释 . 典型的CPU具有多个级别的内存访问 . 有一个内存管道,几级缓存,然后是RAM等 .

    Membar说明齐平管道 . 它们不会改变执行读写的顺序,它只会强制在给定时刻执行优秀的执行 . 它对多线程程序很有用,但不是很多 .

    缓存通常在CPU之间自动一致 . 如果想确保缓存与RAM同步,则需要缓存刷新 . 它与膜非常不同 .

  • 5

    编译器需要在 volatile 访问周围引入一个内存栅栏,当且仅当在该特定平台上对标准工作( setjmp ,信号处理程序等)中指定的 volatile 的使用所必需时 .

    请注意,某些编译器确实超出了C标准所要求的范围,以使 volatile 在这些平台上更强大或更有用 . 可移植代码不应依赖 volatile 来执行超出C标准中指定的任何操作 .

  • 4

    我总是在中断服务程序中使用volatile,例如ISR(通常是汇编代码)修改一些内存位置,在中断上下文之外运行的更高级代码通过指向volatile的指针访问内存位置 .

    我这样做是为了RAM以及内存映射的IO .

    基于此处的讨论,似乎这仍然是volatile的有效使用,但与多线程或CPU没有任何关系 . 如果微控制器的编译器“知道”不能进行任何其他访问(例如,每个访问都在片上,没有缓存,并且只有一个核心)我会认为根本不暗示内存栅栏,编译器只需要防止某些优化 .

    当我们将更多的东西堆积到执行目标代码的“系统”中时,几乎所有的赌注都被关闭了,至少我是如何看待这个讨论的 . 编译器如何涵盖所有基础?

  • 7

    我认为围绕易失性和指令重新排序的混淆源于CPU重新排序的2个概念:

    • 无序执行 .

    • 其他CPU看到的内存读/写顺序(在每个CPU可能看到不同序列的意义上重新排序) .

    易失性会影响编译器生成代码的方式(假设是单线程执行)(包括中断) . 它并不暗示有关内存屏障指令的任何内容,但它相当于阻止编译器执行与内存访问相关的某些类型的优化 .
    一个典型的例子是从内存中重新获取一个值,而不是使用一个缓存在寄存器中的值 .

    无序执行

    CPU可以无序地/推测性地执行指令,只要最终结果可能发生在原始代码中 . CPU可以执行编译器中不允许的转换,因为编译器只能执行在所有情况下都正确的转换 . 相反,CPU可以检查这些优化的有效性,如果结果不正确则退出它们 .

    其他CPU看到的内存读/写顺序

    一系列指令(有效顺序)的最终结果必须与编译器生成的代码的语义一致 . 但是,CPU选择的实际执行顺序可能不同 . 在其他CPU中看到的有效顺序(每个CPU可以具有不同的视图)可能受到内存障碍的限制 .
    我知道内存障碍可以阻止CPU执行无序执行的程度 .

    资料来源:

  • 21

    我正在使用现代OpenGL开发3D图形和游戏引擎开发的在线可下载视频教程 . 我们确实在我们的一个 class 中使用了 volatile . 教程网站可以找到here,使用 volatile 关键字的视频可以在 Shader Engine 系列视频98中找到 . 这些作品不是我自己的,但是被认可为 Marek A. Krzeminski, MASc ,这是视频下载页面的摘录 .

    “由于我们现在可以让我们的游戏在多个线程中运行,因此在线程之间正确同步数据非常重要 . 在本视频中,我将展示如何创建一个volitile锁定类以确保volitile变量正确同步...”

    如果您订阅了他的网站并且可以在此视频中访问他的视频,那么他会参考article关于 Volatilemultithreading 编程的使用 .

    以下是上面链接中的文章:http://www.drdobbs.com/cpp/volatile-the-multithreaded-programmers-b/184403766

    volatile:多线程程序员最好的朋友作者:Andrei Alexandrescu,2001年2月1日volatile关键字的设计是为了防止在某些异步事件出现时可能导致代码不正确的编译器优化 . 我不想破坏你的心情,但本专栏解决了多线程编程的可怕主题 . 如果 - 正如Generic的前一部分所说的那样 - 异常安全编程很难,那么与多线程编程相比,它就是孩子的游戏 . 众所周知,使用多个线程的程序很难编写,证明是正确的,调试,维护和驯服 . 不正确的多线程程序可能会运行多年没有故障,只是意外地运行,因为已经满足一些关键的时序条件 . 毋庸置疑,编写多线程代码的程序员需要得到所有帮助 . 本专栏重点介绍竞争条件 - 多线程程序中常见的问题来源 - 并为您提供有关如何避免它们的见解和工具,令人惊讶的是,让编译器努力帮助您解决这个问题 . 只是一个小关键字尽管C和C标准在线程方面都非常明显,但它们确实以volatile关键字的形式对多线程做出了一点让步 . 就像它更熟知的对应const一样,volatile是一个类型修饰符 . 它旨在与在不同线程中访问和修改的变量结合使用 . 基本上,没有volatile,编写多线程程序变得不可能,或者编译器浪费了大量的优化机会 . 按顺序解释 . 请考虑以下代码:class Gadget {
    上市:
    void Wait(){
    while(!flag_){
    睡眠(1000); //睡眠1000毫秒
    }
    }
    void Wakeup()

    ...
    私人的:
    bool flag_;
    };
    Gadget :: Wait上面的目的是每秒检查一次flag_成员变量,并在另一个线程将该变量设置为true时返回 . 至少这是程序员的意图,但是,唉,等待是不正确的 . 假设编译器发现Sleep(1000)是对外部库的调用,不能修改成员变量flag_ . 然后编译器得出结论,它可以将flag_缓存在寄存器中并使用该寄存器而不是访问较慢的板载内存 . 这是对单线程代码的一个很好的优化,但在这种情况下,它会损害正确性:在您调用Wait for some Gadget对象之后,虽然另一个线程调用Wakeup,但Wait将永远循环 . 这是因为flag_的更改不会反映在缓存flag_的寄存器中 . 优化太乐观了 . 寄存器中的缓存变量是一种非常有 Value 的优化,大部分时间都适用,因此浪费它会很遗憾 . C和C为您提供了明确禁用此类缓存的机会 . 如果对变量使用volatile修饰符,则编译器不会将该变量缓存在寄存器中 - 每次访问都将访问该变量的实际内存位置 . 因此,要使Gadget的Wait / Wakeup组合工作所需要做的就是适当地限定flag_:class Gadget {
    上市:
    ... 如上 ...
    私人的:
    volatile bool flag_;
    };
    关于volatile的基本原理和用法的大多数解释都在这里停止,并建议你使用volatile来限定你在多个线程中使用的原始类型 . 但是,你可以用volatile做更多的事情,因为它是C的精彩类型系统的一部分 . 将volatile与用户定义的类型一起使用您可以不仅对基本类型进行volatile限定,还可以对用户定义的类型进行限定 . 在这种情况下,volatile以类似于const的方式修改类型 . (您也可以同时将const和volatile应用于同一类型 . )与const不同,volatile可以区分基本类型和用户定义类型 . 也就是说,与类不同,原始类型在volatile限定时仍然支持它们的所有操作(加法,乘法,赋值等) . 例如,您可以将非易失性int分配给volatile int,但不能将非易失性对象分配给易失性对象 . 让我们举例说明volatile如何在用户定义的类型上工作 . class Gadget {
    上市:
    void Foo()volatile;
    void Bar();
    ...
    私人的:
    String name_;
    int state_;
    };
    ...
    小工具regularGadget;
    volatile Gadget volatileGadget;
    如果你认为volatile对于对象没那么有用,那就准备好了 . volatileGadget.Foo(); //好的,要求挥发性的乐趣
    //易失物体
    regularGadget.Foo(); //好的,要求挥发性的乐趣
    //非易失性对象
    volatileGadget.Bar(); //错误!要求非易失性功能
    //易失物体!
    从非限定类型到其易失性类型的转换是微不足道的 . 但是,就像使用const一样,您无法从volatile变为不合格 . 您必须使用强制转换:Gadget&ref = const_cast <Gadget&>(volatileGadget);
    ref.Bar(); // 好
    volatile限定类只允许访问其接口的子集,这是一个受类实现者控制的子集 . 用户只能通过使用const_cast获得对该类型接口的完全访问权限 . 此外,就像constness一样,volatile会从类传播到其成员(例如,volatileGadget.name_和volatileGadget.state_是volatile变量) . 易失性,关键部分和竞争条件多线程程序中最简单和最常用的同步设备是互斥锁 . 互斥体公开了Acquire和Release原语 . 一旦你在某个线程中调用Acquire,任何其他调用Acquire的线程都会阻塞 . 后来,当该线程调用时发布,恰好在获取调用中被阻止的一个线程将被释放 . 换句话说,对于给定的互斥锁,只有一个线程可以在对Acquire的调用和对Release的调用之间获得处理器时间 . 对Acquire的调用和对Release的调用之间的执行代码称为临界区 . (Windows术语有点令人困惑,因为它将互斥锁本身称为关键部分,而“互斥锁”实际上是一个进程间互斥锁 . 如果将它们称为线程互斥锁并处理互斥锁,那将会很好 . )互斥锁用于保护针对竞争条件的数据 . 根据定义,当更多线程对数据的影响取决于线程的调度方式时,就会出现竞争条件 . 当两个或多个线程竞争使用相同数据时,会出现竞争条件 . 由于线程可以在任意时刻相互中断,因此数据可能会被破坏或被误解 . 因此,必须使用关键部分小心保护对数据的更改和访问 . 在面向对象的编程中,这通常意味着您将一个互斥锁存储在一个类中作为成员变量,并在您访问该类的状态时使用它 . 经验丰富的多线程程序员可能会打哈欠阅读上面两段,但他们的目的是提供智力锻炼,因为现在我们将与易变连接联系起来 . 我们通过在C类型的世界和线程语义世界之间绘制一个并行来实现这一点 . 在关键部分之外,任何线程都可能随时中断任何其他线程;没有控制,因此可从多个线程访问的变量是易失性的 . 这符合volatile的原始意图 - 防止编译器无意中一次缓存多个线程使用的值 . 在由互斥锁定义的临界区内,只有一个线程可以访问 . 因此,在关键部分内,执行代码具有单线程语义 . 受控变量不再易失 - 您可以删除volatile限定符 . 简而言之,线程之间共享的数据在关键部分之外在概念上是易失性的,而在关键部分内是非易失性的 . 您可以通过锁定互斥锁来输入关键部分 . 通过应用const_cast从类型中删除volatile限定符 . 如果我们设法将这两个操作放在一起,我们就在C的类型系统和应用程序的线程语义之间 Build 连接 . 我们可以让编译器为我们检查竞争条件 . LockingPtr我们需要一个收集互斥锁获取和const_cast的工具 . 让我们开发一个LockingPtr类模板,使用volatile对象obj和mutex mtx进行初始化 . 在其生命周期中,LockingPtr保持mtx获得 . 此外,LockingPtr提供对易失性剥离的obj的访问 . 通过operator->和operator 以智能指针方式提供访问 . const_cast在LockingPtr中执行 . 强制转换在语义上是有效的,因为LockingPtr会保留在其生命周期内获取的互斥锁 . 首先,让我们定义一个类Mutex的骨架,LockingPtr将使用它:class Mutex {
    上市:
    void Acquire();
    void Release();
    ...
    };
    要使用LockingPtr,可以使用操作系统的本机数据结构和原始函数实现Mutex . LockingPtr使用受控变量的类型进行模板化 . 例如,如果要控制Widget,则使用使用volatile Widget类型的变量初始化的LockingPtr . LockingPtr的定义非常简单 . LockingPtr实现了一个简单的智能指针 . 它只专注于收集const_cast和一个关键部分 . template <typename T>
    class LockingPtr {
    上市:
    //构造函数/析构函数
    LockingPtr(volatile T&obj,Mutex&mtx)
    :pObj_(const_cast <T >(&obj)),pMtx (&mtx){
    mtx.Lock();
    }
    ~LockingPtr(){
    pMtx ->解锁();
    }
    //指针行为
    T&operator *(){
    return * pObj
    ;
    }
    T * operator - >(){
    return pObj
    ;
    }
    私人的:
    T * pObj_;
    互斥
    pMtx_;
    LockingPtr(const LockingPtr&);
    LockingPtr&operator =(const LockingPtr&);
    };
    尽管它很简单,但LockingPtr在编写正确的多线程代码时非常有用 . 您应该将线程之间共享的对象定义为volatile,并且永远不要将const_cast与它们一起使用 - 始终使用LockingPtr自动对象 . 让我们举一个例子来说明这一点 . 假设您有两个共享矢量对象的线程:class SyncBuf {
    上市:
    void Thread1();
    void Thread2();
    私人的:
    typedef vector <char> BufT;
    volatile BufT buffer_;
    Mutex mtx_; //控制对buffer_的访问
    };
    在线程函数中,您只需使用LockingPtr来获取对buffer_ member变量的受控访问:void SyncBuf :: Thread1(){
    LockingPtr <BufT> lpBuf(buffer_,mtx_);
    BufT :: iterator i = lpBuf-> begin();
    for(; i!= lpBuf-> end(); i)

    }
    代码很容易编写和理解 - 无论何时需要使用buffer_,都必须创建一个指向它的LockingPtr . 一旦你这样做,你就可以访问矢量的整个界面 . 好的部分是,如果你犯了一个错误,编译器会指出它:void SyncBuf :: Thread2(){
    //错误!无法访问volatile对象的“begin”
    BufT :: iterator i = buffer_.begin();
    //错误!无法访问volatile对象的“end”
    for(; i!= lpBuf-> end(); i)

    }
    在应用const_cast或使用LockingPtr之前,您无法访问buffer_的任何功能 . 不同之处在于LockingPtr提供了将const_cast应用于volatile变量的有序方法 . LockingPtr非常具有表现力 . 如果只需要调用一个函数,则可以创建一个未命名的临时LockingPtr对象并直接使用它:unsigned int SyncBuf :: Size(){
    return LockingPtr <BufT>(buffer_,mtx ) - > size();
    }
    回到原始类型我们看到了如何很好地保护对象免受不受控制的访问,以及LockingPtr如何提供一种编写线程安全代码的简单有效方法 . 现在让我们回到原始类型,由volatile来区别对待 . 让我们考虑一个多线程共享int类型变量的例子 . class 专柜{
    上市:
    ...
    void Increment(){ctr
    ; }
    void Decrement(){ - ctr_; }
    私人的:
    int ctr_;
    };
    如果要从不同的线程调用Increment和Decrement,则上面的片段是错误的 . 首先,ctr_必须是易变的 . 其次,即使像ctr_这样的看似原子的操作实际上也是一个三阶段的操作 . 内存本身没有算术功能 . 递增变量时,处理器:读取寄存器中的变量递增寄存器中的值将结果写回存储器这三步操作称为RMW(读 - 修改 - 写) . 在RMW操作的Modify部分期间,大多数处理器释放内存总线,以便让其他处理器访问内存 . 如果此时另一个处理器对同一个变量执行RMW操作,我们就会遇到竞争条件:第二个写入会覆盖第一个写入的效果 . 为避免这种情况,您可以再次依赖LockingPtr:class Counter {
    上市:
    ...
    void Increment(){
    LockingPtr <int>(ctr_,mtx_); }
    void Decrement(){ - * LockingPtr <int>(ctr ,mtx); }
    私人的:
    volatile int ctr_;
    Mutex mtx_;
    };
    现在代码是正确的,但与SyncBuf的代码相比,它的质量较差 . 为什么?因为使用Counter,如果您错误地直接访问ctr_(没有锁定它),编译器将不会警告您 . 如果ctr_是易失性的,编译器会编译ctr_,尽管生成的代码完全不正确 . 编译器不再是你的盟友,只有你的注意力可以帮助你避免竞争条件 . 那你该怎么办?只需封装您在更高级别结构中使用的原始数据,并将volatile与这些结构一起使用 . 矛盾的是,直接使用volatile与内置函数更糟糕,尽管最初这是volatile的使用意图! volatile成员函数到目前为止,我们已经有了聚合易变数据成员的类;现在让我们考虑设计类,这些类反过来将成为更大对象的一部分并在线程之间共享 . 这里是volatile成员函数可以提供很大帮助的地方 . 在设计类时,volatile只限定那些线程安全的成员函数 . 您必须假设来自外部的代码将随时从任何代码调用volatile函数 . 不要忘记:volatile等于免费的多线程代码,没有关键部分;非易失性等于单线程场景或在关键部分内 . 例如,您定义了一个类Widget,它以两种变体实现操作 - 一个是线程安全的,一个是快速的,不受保护的 . class Widget {
    上市:
    void Operation()volatile;
    void Operation();
    ...
    私人的:
    Mutex mtx_;
    };
    注意使用重载 . 现在,Widget的用户可以使用统一语法调用Operation,用于易失性对象和获取线程安全性,或者用于常规对象并获得速度 . 用户必须小心将共享Widget对象定义为volatile . 实现volatile成员函数时,第一个操作通常是使用LockingPtr锁定它 . 然后通过使用非易失性兄弟来完成工作:void Widget :: Operation()volatile {
    LockingPtr <Widget> lpThis(* this,mtx_);
    lpThis->操作(); //调用非易失性函数
    }
    总结编写多线程程序时,可以使用volatile来获得优势 . 您必须遵守以下规则:将所有共享对象定义为volatile . 不要直接使用volatile与原始类型 . 定义共享类时,使用volatile成员函数来表示线程安全性 . 如果你这样做,并且你使用简单的通用组件LockingPtr,你可以编写线程安全的代码,而不用担心竞争条件,因为编译器会为你担心,并会努力指出你错的地方 . 我参与过的几个项目使用volatile和LockingPtr效果很好 . 代码干净,易懂 . 我记得有几个死锁,但我更喜欢死锁,因为它们更容易调试 . 几乎没有与竞争条件有关的问题 . 但是你永远不会知道 . 致谢非常感谢James Kanze和Sorin Jianu,他们提供了富有洞察力的想法 . Andrei Alexandrescu是RealNetworks Inc.(www.realnetworks.com)的开发经理,总部位于华盛顿州西雅图,是着名的Modern C Design一书的作者 . 可以通过www.moderncppdesign.com与他联系 . Andrei也是The C Seminar(www.gotw.ca/cpp_seminar)的特色讲师之一 .

    这篇文章可能有点陈旧,但它确实很好地理解了使用volatile修饰符和使用多线程编程来帮助保持事件异步,同时让编译器为我们检查竞争条件 . 这可能无法直接回答OP关于创建内存栅栏的原始问题,但我选择将此作为其他人的答案发布,作为在使用多线程应用程序时充分利用volatile的一个很好的参考 .

  • 0

    关键字 volatile 本质上意味着读取和写入对象应该是 performed exactly as written by the program, and not optimized in any way . 二进制代码应该遵循C或C代码:读取它的负载,存储写入的存储 .

    这也意味着不应该期望读取产生可预测的值:编译器在写入相同的volatile对象之后甚至不应立即采取任何关于读取的内容:

    volatile int i;
    i = 1;
    int j = i; 
    if (j == 1) // not assumed to be true
    

    volatile 可能是 the most important tool in the "C is a high level assembly language" toolbox .

    声明对象volatile是否足以确保处理异步更改的代码行为取决于平台:不同的CPU为正常的内存读取和写入提供不同级别的保证同步 . 除非您是该领域的专家,否则您可能不应该尝试编写这种低级多线程代码 .

    原子基元为多线程提供了一个很好的更高级别的对象视图,可以很容易地推理代码 . 几乎所有程序员都应该使用原子基元或原语来提供互斥,如互斥,读写锁,信号量或其他阻塞原语 .

相关问题