首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >问答首页 >C++标准:可以在互斥锁之上解除轻松的原子存储吗?

C++标准:可以在互斥锁之上解除轻松的原子存储吗?
EN

Stack Overflow用户
提问于 2017-08-03 05:04:11
回答 6查看 1.2K关注 0票数 16

在标准中有什么措辞可以保证原子的轻松存储不会被提升到互斥锁之上?如果没有,是否有明确规定编译器或CPU可以这么做的措辞?

例如,以下面的程序为例(它可能对foo_has_been_set使用acq/rel,并避免锁,并/或使foo本身具有原子性。它是这样写的来说明这个问题。)

代码语言:javascript
运行
复制
std::mutex mu;
int foo = 0;  // Guarded by mu
std::atomic<bool> foo_has_been_set{false};

void SetFoo() {
  mu.lock();
  foo = 1;
  foo_has_been_set.store(true, std::memory_order_relaxed);
  mu.unlock();
}

void CheckFoo() {
  if (foo_has_been_set.load(std::memory_order_relaxed)) {
    mu.lock();
    assert(foo == 1);
    mu.unlock();
  }
}

如果另一个线程同时调用CheckFoo,那么SetFoo是否有可能在上面的程序中崩溃,或者是否可以保证编译器和CPU不能在调用mu.lock之前取消对foo_has_been_set的存储?

这与一个更老的问题有关,但我并不十分清楚答案是否适用于此。特别是,该问题答案中的反示例可能适用于对SetFoo的两个并发调用,但我对编译器知道有一个调用SetFoo和一个调用CheckFoo的情况感兴趣。能保证安全吗?

我在找标准中的具体引文。

EN

回答 6

Stack Overflow用户

回答已采纳

发布于 2017-08-04 04:54:13

我想我已经找出了保证程序不会崩溃的特殊的局部命令边。在下面的答案中,我参考了标准草案的版本N4659

编写线程A和读取器线程B所涉及的代码是:

代码语言:javascript
运行
复制
A1: mu.lock()
A2: foo = 1
A3: foo_has_been_set.store(relaxed)
A4: mu.unlock()

B1: foo_has_been_set.load(relaxed) <-- (stop if false)
B2: mu.lock()
B3: assert(foo == 1)
B4: mu.unlock()

我们寻求一个证明,如果B3执行,那么A2就会发生在B3之前,正如[intro.races]/10中所定义的那样。通过[intro.races]/10.2,可以证明A2线程间发生在B3之前。

由于在给定互斥体上的锁定和解锁操作是以单个总顺序([thread.mutex.requirements.mutex]/5)进行的,所以我们必须首先使用A1或B2。这两起案件:

  1. 假设A1发生在B2之前。然后,通过[thread.mutex.class]/1[thread.mutex.requirements.mutex]/25,我们知道A4将与B2同步。因此,通过[intro.races]/9.1,A4线程间发生在B2之前.由于B2是在B3之前进行排序的,所以[intro.races]/9.3.1知道A4线程间的顺序发生在B3之前。由于A2是在A4之前进行排序的,而由[intro.races]/9.3.2进行的,所以A2线程间的顺序发生在B3之前.
  2. 假设B2发生在A1之前。然后,按照与上面相同的逻辑,我们知道B4与A1同步。因此,由于A1是在A3之前进行排序的,而由[intro.races]/9.3.1执行的,B4线程间的顺序发生在A3之前.因此,由于B1是在B4之前进行排序的,而由[intro.races]/9.3.2执行的,B1线程间的顺序发生在A3之前.因此,通过[intro.races]/10.2,B1发生在A3之前。但根据[intro.races]/16的说法,B1必须从前A3状态取值。因此,加载将返回false,B2从一开始就不会运行。换句话说,这个案子是不可能发生的。

因此,如果B3完全执行(案例1),则在B3和断言通过之前执行A2。∎

票数 9
EN

Stack Overflow用户

发布于 2017-08-03 07:15:15

互斥保护区域内的内存操作不能“逃离”该区域。这适用于所有内存操作,原子操作和非原子操作。

第1.10.1节:

获取互斥的呼叫将在相应包含互斥的位置上执行获取操作,释放相同互斥的呼叫将在这些相同的位置上执行释放操作。

此外,在第1.10.1.6节中:

给定互斥体上的所有操作都是以一个总顺序进行的。每个互斥锁的获取都“读取”上一个互斥体发行版所写的值。

以及在30.4.3.1

互斥对象有助于防止数据竞争,并允许执行代理之间的数据安全同步。

这意味着,获取(锁定)互斥锁设置了单向屏障,以防止在获取(在受保护区域内)之后被排序的操作在互斥锁上移动。

释放(解锁)互斥设置单向屏障,以防止在释放之前(在保护区域内)的操作在互斥锁上向下移动。

此外,由互斥体释放的内存操作与获取相同互斥的另一个线程同步(可见)。

在您的示例中,foo_has_been_set是在CheckFoo中签入的。如果它读取true,您知道值1已由SetFoo分配给foo,但它尚未同步。下面的互斥锁将获得foo,同步完成,断言无法触发。

票数 3
EN

Stack Overflow用户

发布于 2019-12-15 15:45:18

标准不直接保证这一点,但是您可以在thread.mutex.requirements.mutex行之间读取它:

为了确定数据竞争的存在,它们表现为原子操作(intro.multithread)。 在单个互斥锁上的锁定和解锁操作应以单个总顺序进行。

现在第二句看起来像一个很难保证的,但它确实不是。单一的总顺序是非常好的,但它只意味着有一个明确定义的获取和释放一个特定互斥物的总顺序。这并不意味着任何原子操作或相关的非原子操作的影响都应该或必须在与互斥体相关的特定点上全局可见。或者随便吧。唯一得到保证的是代码执行的顺序(具体来说,是对单个函数lockunlock的执行),对于数据可能发生或不可能发生的事情,或者其他方面,没有人说什么。

然而,人们可以在字里行间读到这是“作为原子操作的行为”部分的意图。

从其他地方来看,这也是非常清楚的,这是一个确切的想法,一个实现应该以这种方式工作,而不是明确地说它必须这样做。例如,intro.races读到:

[注释:,例如,获取互斥的呼叫将在包含互斥的位置上执行获取操作。相应地,释放相同互斥的调用将对这些相同的位置执行释放操作。

注意那个不幸的、无害的单词“注意:”。注释是不规范的。所以,虽然很明显,这就是我们想要理解的方式(mutex lock =not;unlock = release),但这是而不是实际上是一个保证。

我认为最好的,虽然不是直截了当的保证来自于这句话在thread.mutex.requirements.general。

互斥对象方便了对数据竞争的的保护,并允许执行代理之间的数据安全同步。

所以,这就是互斥体所做的事情(没有说明具体是如何做到的)。它可以防止数据竞争。完全停止。

因此,不管你想出了什么微妙之处,不管你写了什么或者没有明确地说出什么,使用互斥对象可以防止数据竞争(.因为没有给出特定的类型)。这就是我们所写的。因此,总之,只要你使用互斥,你就可以很好地去,即使是轻松的排序,或根本没有原子操作。负载和存储(任何类型的)都不能移动,因为这样您就无法确定没有发生数据竞争。然而,这正是互斥物所能保护的。

因此,不必这么说,这意味着互斥必须是一个完全的

票数 1
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/45475241

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档