RError.com

RError.com Logo RError.com Logo

RError.com Navigation

  • 主页

Mobile menu

Close
  • 主页
  • 系统&网络
    • 热门问题
    • 最新问题
    • 标签
  • Ubuntu
    • 热门问题
    • 最新问题
    • 标签
  • 帮助
主页 / 问题 / 1052530
Accepted
ibse
ibse
Asked:2020-11-30 17:18:57 +0000 UTC2020-11-30 17:18:57 +0000 UTC 2020-11-30 17:18:57 +0000 UTC

来自不同线程的共享变量的可见性

  • 772

我无法处理内存障碍的话题。什么时候,哪些线程,以什么顺序更新共享变量的值?

bool flag = false;

void f1() {
    flag = true;
}

void f2() {
    cout << flag << endl;
}

int main() {
    thread FIRST_THREAD(f1); //Первый поток
    thread SECOND_THREAD(f2); //Второй поток

    FIRST_THREAD.join();
    SECOND_THREAD.join();
}

是否会发生在第一个线程执行f1()后更改flag为true第二个线程执行f2()后读取旧值flag并打印false?

如果是这样,应该如何设置内存屏障来避免这种情况?所以?:

void f1() {
    atomic_thread_fence(memory_order_release);
    flag = true;
}

void f2() {
    cout << flag << endl;
    atomic_thread_fence(memory_order_acquire);
}

它与以下有何不同:

void f1() {
    atomic_thread_fence(memory_order_seq_cst);
    flag = true;
}

void f2() {
    cout << flag << endl;
    atomic_thread_fence(memory_order_seq_cst);
}

据我所知,该变体release/acquire比seq_cst. “较弱”是什么意思?

升级版:

@Andrej Levkovitch,首先,我自己不知道我的问题是否有问题,所以我问的第一件事是“会不会是……?” 找出是否有问题。我马上会注意到,问题不在于flag通过互斥锁、原子变量等同步访问。在我描述的假设线程执行场景中,默认情况下会发生访问同步flag:第一个线程总是flag在第二个线程读取之前写入flag。问题不同:第一个线程能否将值更改flag为,之后true的第二个线程(即有同步)不更新其值并读取旧值flagfalse. “在家”是指(虽然我不确定我在这里写的是什么)线程正在运行的处理器内核的缓存。

c++
  • 2 2 个回答
  • 10 Views

2 个回答

  • Voted
  1. Best Answer
    Fat-Zer
    2020-12-01T05:25:04Z2020-12-01T05:25:04Z

    一些重要的非显而易见的事实

    • 内存屏障(内存屏障,栅栏)和屏障(屏障)是不同的东西。因为 在俄语中还没有像栅栏这样的单音节术语,进一步说的障碍我将只指记忆障碍。
    • C++ 中的障碍有几个目的:
      • 告诉编译器它可以多么自由地重新排列指令。
      • 在某些平台上告诉处理器相同(在 x86 上不需要)。
      • 向您的代码添加额外的系统特定指令,以确保相关对象的内存一致性。(在 x86 上未添加此类指令)
    • 要观察内存屏障对程序或编译器行为的影响,您至少需要两个共享对象。对于一个变量,是否使用障碍没有实际区别。
    • 大多数对原子变量的操作都隐含地包含一个内存屏障(但它们并不完全相同)。
    • std::atomic_thread_fence允许您使这些操作更加优化。
    • 严格来说,C++只能与原子操作一起工作std::atomic_thread_fence。没有它们,语言不提供任何保证。

    什么时候需要内存屏障?

    假设我们有两个绑定变量:

    const char *msg = 0;
    int ready = 0;
    

    在一个线程中,分配发生(IRL 可以被认为是原子的):

    void post (const char *my_msg) {
      msg = my_msg;
      ready = 1;
    }
    

    在第二个线程中,检查值:

    void print () {
      if (ready) {
        puts (msg);
      }
    }
    

    在市侩级别,似乎应该始终显示该行。在第一个线程中,首先ready赋值,然后才赋值msg。但是,首先,编译器有权重新排列赋值msg和ready位置,其次,一些处理器本身也可以在运行时重新排列这些操作;所以第二个线程可能会发现自己处于ready == 1, 和msg == 0.

    内存屏障正是编译器和处理器都不会重新排列操作的界线。那些。第一个线程应如下所示:

    void post (const char *my_msg) {
      msg = "hello world!";
      release_fence(); //< псевдокод
      ready = 1;
    }
    

    在第二个流中,还需要一个屏障,因为。编译器(或处理器,更有可能)可以把它变成这样的东西:

    const char *my_msg = msg;
    if (ready) {
      puts (my_msg);
    }
    

    因此,您需要执行以下操作:

    void print () {
      if (ready) {
        acquire_fence(); //< псевдокод
        puts (my_msg);
      }
    }
    

    为什么std::atomic_thread_fence不是每一步都使用它?

    事实是,一方面,所有memory_order_relaxed使用原子变量 ( std::atomic) 的操作(除了那些显式标记的操作)都会隐式生成内存屏障,另一方面,使用std::atomic_thread_fence需要同时使用原子变量。print ()例如,从上面的示例中绝对正确,如下所示:

    std::atomic<int> ready = 0;
    
    // ...
    
    void atomic_print () {
      if (ready.load()) {
        puts (msg);
      }
    }
    

    那为什么还需要它std::atomic_thread_fence呢?

    它允许您给编译器和处理器更多的优化自由度。而对于程序员来说,更准确地描述他想要什么。例如atomic_print (),从上面的示例中,它实际上执行以下操作序列(伪代码):

    int local_ready = ready;
    fence();
    if (local_ready) {
      puts (msg);
    }
    

    这里的问题是,fence()无论那里是否需要访问,它都会执行msg,并且在某些架构师中,这条指令可能非常昂贵。std::atomic_thread_fence让我们将其重写为:

    void fence_print () {
      if (ready.load(std::memory_order_relaxed)) {
        std::atomic_thread_fence (std::memory_order_acquire);
        puts (msg);
      }
    }
    

    release / acquire / seq_cst 障碍有什么区别?

    不同之处在于编译器可以通过它们重新排列哪些指令。

    • seq_cst- 最严格的。它保证不会通过它重新排列任何指令(既不保存也不加载)(既不向前也不向后)。

      load  (a)-----------+
      store (b)-------+   |
                      v   v
      --- seq_cst --- X X X X
                        ^   ^
      load  (c)---------+   |
      store (d)-------------+
      
    • release- 确保屏障之后的所有指令(读取和写入)不会在屏障之前的写指令 之前重新排列:

      load  (a)---------+
      store (b)         |
      --- release --- X | X
                      ^ | ^
      load  (c)-------+ | |
               <--------+ |
      store (d)-----------+
      
    • acquire- 相反,它保证在屏障之前的任何指令(无论是读取还是写入)都不会在屏障之后的任何读取指令 之后传递:

             <------------+ 
      load  (a)---------+ |
      store (b)-------+ | |
                      v v |
      --- acquire --- X X |
      load  (c)           |
      store (d)-----------+
      

    什么时候用什么?

    • 如果这不是速度关键段,那么您始终可以使用默认选项seq_cst。在其他情况下(例如,在创建加载的非阻塞原语时),简而言之,通常值得在保存时(在保存的最后)release和加载时(在开始时)使用 - acquire。实际上,学习区分何时使用一种语义以及何时使用另一种语义是创建非阻塞系统时的主要任务之一。

    一字答题

    难道是在第一个线程通过执行 f1() 将 flag 更改为 true 之后,第二个线程通过执行 f2() 读取 flag 的旧值并打印 false?

    一般来说是可以的,但是这里没关系。这种情况与f2()在 f1() 之前执行过时的变体无法区分。

    据我所知,release/acquire 比 seq_cst 更高效,更弱。“较弱”是什么意思?

    它对编译器/处理器施加的限制更少。

    问题不同:第一个线程能否将标志值更改为真,之后的第二个线程(即有同步)不更新标志值并读取旧的假值。“在家”是指(虽然我不确定我在这里写的是什么)线程正在运行的处理器内核的缓存。

    假设它可以,但我无法说出会发生这种情况的真实架构。在 x86 上,它绝对不能。


    有趣的信息。

    • 不错的博客,有几篇关于该主题的文章。[英文]
    • 关于非阻塞结构的视频讲座。[eng]
    • 5
  2. Andrej Levkovitch
    2020-11-30T18:04:17Z2020-11-30T18:04:17Z

    首先,我想说,使用全局变量(在绝大多数情况下)是一种恶毒的做法。并且全局变量是由线程共享的——一般来说,你应该有一个很好的理由。但是既然它发生了,而且你需要共享一些简单类型的变量(意思是 int、bool、...),那么最好使用 atomic。标准库包含 bool 的原子 - 在这种情况下,这将是最好的解决方案。如果你需要共享某个类的变量,那么你已经需要和互斥锁一起使用了:锁定与变量关联的互斥锁并改变变量。

    • 1

相关问题

  • C++ 和循环依赖

Sidebar

Stats

  • 问题 10021
  • Answers 30001
  • 最佳答案 8000
  • 用户 6900
  • 常问
  • 回答
  • Marko Smith

    根据浏览器窗口的大小调整背景图案的大小

    • 2 个回答
  • Marko Smith

    理解for循环的执行逻辑

    • 1 个回答
  • Marko Smith

    复制动态数组时出错(C++)

    • 1 个回答
  • Marko Smith

    Or and If,elif,else 构造[重复]

    • 1 个回答
  • Marko Smith

    如何构建支持 x64 的 APK

    • 1 个回答
  • Marko Smith

    如何使按钮的输入宽度?

    • 2 个回答
  • Marko Smith

    如何显示对象变量的名称?

    • 3 个回答
  • Marko Smith

    如何循环一个函数?

    • 1 个回答
  • Marko Smith

    LOWORD 宏有什么作用?

    • 2 个回答
  • Marko Smith

    从字符串的开头删除直到并包括一个字符

    • 2 个回答
  • Martin Hope
    Alexandr_TT 2020年新年大赛! 2020-12-20 18:20:21 +0000 UTC
  • Martin Hope
    Alexandr_TT 圣诞树动画 2020-12-23 00:38:08 +0000 UTC
  • Martin Hope
    Air 究竟是什么标识了网站访问者? 2020-11-03 15:49:20 +0000 UTC
  • Martin Hope
    Qwertiy 号码显示 9223372036854775807 2020-07-11 18:16:49 +0000 UTC
  • Martin Hope
    user216109 如何为黑客设下陷阱,或充分击退攻击? 2020-05-10 02:22:52 +0000 UTC
  • Martin Hope
    Qwertiy 并变成3个无穷大 2020-11-06 07:15:57 +0000 UTC
  • Martin Hope
    koks_rs 什么是样板代码? 2020-10-27 15:43:19 +0000 UTC
  • Martin Hope
    Sirop4ik 向 git 提交发布的正确方法是什么? 2020-10-05 00:02:00 +0000 UTC
  • Martin Hope
    faoxis 为什么在这么多示例中函数都称为 foo? 2020-08-15 04:42:49 +0000 UTC
  • Martin Hope
    Pavel Mayorov 如何从事件或回调函数中返回值?或者至少等他们完成。 2020-08-11 16:49:28 +0000 UTC

热门标签

javascript python java php c# c++ html android jquery mysql

Explore

  • 主页
  • 问题
    • 热门问题
    • 最新问题
  • 标签
  • 帮助

Footer

RError.com

关于我们

  • 关于我们
  • 联系我们

Legal Stuff

  • Privacy Policy

帮助

© 2023 RError.com All Rights Reserve   沪ICP备12040472号-5