详解C++11原子变量、内存顺序及相关引用案例

C++11 将多线程纳入了标准。一旦涉及到多线程,就需要考虑并发,数据竞争 (date race),线程同步等问题。为此 C++ 提供了互斥锁 std::mutex,原子变量 std::atomic 等标准库。

对于原子变量的操作,有一个很重要的概念就是内存顺序 (memory order),其中涉及到的概念很多,理解起来可能会有些困难。本文来从底层详解这个问题。

其中 3.4 节和 3.5 节标注了星号,它们的实际应用较少,不感兴趣的可以先跳过,或者读完全文后再阅读。

1.原子变量

不能在两个线程中同时访问修改一个变量,这会导致数据竞争的问题,程序的结果是未定义的。

从实现上来说,我们不能保证读写操作是原子的,例如 32 位机器上,修改一个 64 位变量可能需要两条指令。或者变量有可能只是在寄存器里,对其的修改要在稍后才会写入内存。解决数据竞争的方式除了使用 std::mutex 加锁,还可以使用原子变量。

//使用原子变量一个案例
std::atomic<int> a{0};

void thread1() {
    a = 1;
}

void thread2() {
    std::cout << a << std::endl;
}

使用原子变量不用担心数据竞争,对它的操作都是原子的。除此之外,原子变量的操作可以指定内存顺序,帮助我们实现线程同步,这也是本文的重点。

对原子变量的操作可以分为三种

  • store 将一个值存到原子变量中。
  • load 读取原子变量中的值。
  • read-modify-write (RMW) 原子地执行读取,修改和写入。如自增操作 fetch_add,交换操作 exchange (返回变量当前的值并写入指定值) 等。

每个原子操作都需要指定一个内存顺序 (memory order),不同的内存顺序有不同的语义, 会实现不同的顺序模型 (order model)

enum memory_order {
    memory_order_relaxed,
    memory_order_consume,
    memory_order_acquire,
    memory_order_release,
    memory_order_acq_rel,
    memory_order_seq_cst,
};

这六种内存顺序相互组合可以实现三种顺序模型:

  • Sequencial consistent ordering 实现同步,且保证全局顺序一致 (single total order) 的模型。
  • Acquire-release ordering 实现同步,但不保证保证全局顺序一致的模型
  • Relaxed ordering 不能实现同步,只保证原子性的模型。

atomic::store 和 atomic::load 函数都有一个内存顺序的参数,默认为 memory_order_seq_cst。它们的声明如下

void store(T desired, std::memory_order order = std::memory_order_seq_cst);
T load(std::memory_order order = std::memory_order_seq_cst) const;

2.基础概念

在开始讲这六种内存顺序之前,有必要先了解一下几个最基础的概念。

2.1 修改顺序

对一个原子变量的所有修改操作总是存在一定的先后顺序,且所有线程都认可这个顺序,即使这些修改操作是在不同的线程中执行的。这个所有线程一致同意的顺序就称为修改顺序 (modification order),这意味着

  • 两个修改操作不可能同时进行,一定存在一个先后顺序。这很容易理解,因为这是原子操作必须保证的,否则就有数据竞争的问题。
  • 即使每次运行的修改顺序可能都不同,但所有线程看到的修改顺序总是一致的。如果线程 a 看到原子变量 x 由 1 变成 2,那么线程 b 就不可能看到 x 由 2 变成 1。

无论使用哪种内存顺序,原子变量的操作总能满足修改顺序一致性,即使是最松散的 memory_order_relaxed。

std::atomic<int> a{0};

void thread1() {
    for (int i = 0; i < 10; i += 2)
        a.store(i, std::memory_order_relaxed);
}

void thread2() {
    for (int i = 1; i < 10; i += 2)
        a.store(i, std::memory_order_relaxed);
}

void thread3(vector<int> *v) {
    for (int i = 0; i < 10; ++i)
        v->push_back(a.load(std::memory_order_relaxed));
}

void thread4(vector<int> *v) {
    for (int i = 0; i < 10; ++i)
        v->push_back(a.load(std::memory_order_relaxed));
}

int main() {
    vector<int> v3, v4;
    std::thread t1(thread1), t2(thread2), t3(thread3, &v3), t4(thread4, &v4);
    t1.join(), t2.join(), t3.join(), t4.join();

    for (int i : v3) cout << i << " ";
    cout << endl;
    for (int i : v4) cout << i << " ";
    cout << endl;
    return 0;
}

上面的代码创建了 4 个线程。thread1 和 thread2 分别将偶数和奇数依次写入原子变量 a,thread3 和 thread4 则读取它们。最后输出 thread3 和 thread4 每次读取到的值,程序运行的结果可能是这样的:

//第一次
1 8 7 7 7 9 9 9 9 9
0 2 8 8 8 7 9 9 9 9
//第二次
1 2 5 6 9 9 9 8 8 8
1 3 2 5 9 8 8 8 8 8

虽然每次运行的修改顺序不同,各个线程也不太可能看到每次修改的结果,但是它们看到的修改顺序是一致的。例如 thread3 看到 8 先于 9,thread4 也会看到 8 先于 9,反之亦然。

2.2 先于发生

Happens-before 是一个非常重要的概念,如果操作 a “happens-before” 操作 b, 则操作 a 的结果对于操作 b 可见。happens-before 的关系可以建立在用一个线程的两个操作之间,也可以建立在不同的线程的两个操作之间。

在单线程中只需要sequenced-before即可, 也就是按照语句顺序来写.

a = 42; // (1)
cout << a << endl; // (2)

语句 (1) 在语句 (2) 的前面,因此语句 (1) “sequenced-before” 语句 (2),也就是 (1) “happens-before” 语句 (2)。所以 (2) 可以打印出 (1) 赋值的结果

对于多线程来讲,按照语句顺序即equenced-before来写,也无法达成happens-before。如果我们通过一些手段,让不同线程的两个操作同步,我们称这两个操作之间有 synchronizes-with 的关系。例如以下代码

void thread1() {
    a += 1 // (1)
    unlock(); // (2)
}

void thread2() {
    lock(); // (3)
    cout << a << endl; // (4)
}

假设直到 thread1 执行到 (2) 之前,thread2 都会阻塞在 (3) 处的 lock() 中。那么可以推导出:

  • 根据语句顺序,有 (1) “sequenced-before” (2) 且 (3) “sequenced-before” (4)
  • 因为 (2) “synchronizes-with” (3) 且 (3) “sequenced-before” (4),所以 (2) 先于 (4)
  • 因为 (1) “sequenced-before” (2) 且 (2) 先于 (4) 操作,所以 (1) 先于 (4) 操作,所以 (1) “happens-before” (4)。
    因此 (4) 可以读到 (1) 对变量 a 的修改

2.3 Happens-before 不代表指令实际的执行顺序

需要说明的是,happens-before 是 C++ 语义层面的概念,它并不代表指令在 CPU 中实际的执行顺序。为了优化性能, 编译器会在不破坏语义的前提下对指令重排。例如

extern int a, b;
int add() {
    a++;
    b++;
    return a + b;
}

虽然有 a++; “happens-before” b++; 但编译器实际生成的指令可能是先加载 a,b 两个变量到寄存器,接着分别执行 加一操作,然后再执行 a + b,最后才将自增的结果写入内存。

其实编译器甚至还有可能先自增 b 再自增 a。这样的重排并不会影响语义,两个自增操作的结果仍然对 return a + b; 可见。

换句话说, 虽然在 C++ 语义层面上,a++ 在 b++ 之前发生,但在实际的机器代码执行过程中,这两个操作的顺序可能会被编译器优化器重新安排,只要保证最终的结果与原始代码的语义一致即可。这种优化是合法的,因为它不改变程序的行为,并且可以提高性能。

3. 内存顺序

前面我们提到 C++ 的六种内存顺序相互组合可以实现三种顺序模型。现在我们来具体看看如何使用这六种内存顺序,以及怎样的组合可以实现 synchronizes-with 的关系。

3.1 memory_order_seq_cst

是最强的内存顺序,代表着顺序一致性(memory order sequential consistency),它确保了所有的原子操作都按照程序中的顺序来执行,并且在各个线程之间保持一致的顺序。

memory_order_seq_cst 可以用于 store,load 和 read-modify-write 操作,实现 sequencial consistent 的顺序模型。在这个模型下,所有线程看到的所有操作都有一个一致的顺序,即使这些操作可能针对不同的变量,运行在不同的线程。

2.1 节中我们介绍了修改顺序 (modification order),即单一变量的修改顺序在所有线程看来都是一致的。Sequencial consistent 则将这种一致性扩展到了所有变量。

std::atomic<bool> x{false}, y{ false };

void thread1() {
    x.store(true, std::memory_order_seq_cst); // (1)
}

void thread2() {
    y.store(true, std::memory_order_seq_cst); // (2)
}

std::atomic<int> z{0};

void read_x_then_y() {
    while (!x.load(std::memory_order_seq_cst)); // (3)
    if (y.load(std::memory_order_seq_cst)) ++z; // (4)
}

void read_y_then_x() {
    while (!y.load(std::memory_order_seq_cst)); // (5)
    if (x.load(std::memory_order_seq_cst)) ++z; // (6)
}

int main() {
    std::thread a(thread1), b(thread2), c(read_x_then_y), d(read_y_then_x);
    a.join(), b.join(), c.join(), d.join();
    assert(z.load() != 0); // (7)
}

thread1 和 thread2 分别修改原子变量 x 和 y。运行过程中,有可能先执行 (1) 再执行 (2),也有可能先执行 (2) 后执行 (1)。但无论如何,所有线程中看到的顺序都是一致的。

(7) 处的断言永远不会失败。因为 x 和 y 的修改顺序是全局一致的,如果先执行 (1) 后执行 (2),则 read_y_then_x 中循环 (5) 退出时,能保证 y 为 true。此时 x 也必然为 true,因此 (6) 会被执行。

同理,如果先执行 (2) 后执行 (1),则循环 (3) 退出时 y 也必然为 true, 因此 (4) 会被执行。无论如何,z 最终都不会等于 0。

Sequencial consistent 可以实现 synchronizes-with 的关系。如果一个 memory_order_seq_cst 的 load 操作在某个原子变量上读到了一个 memory_order_seq_cst 的 store 操作在这个原子变量中写入的值,则 store 操作 “synchronizes-with” load 操作。

在上面的例子中, 有 (1) “synchronizes-with” (3) 和 (2) “synchronizes-with” (5)。

实现 sequencial consistent 模型有一定的开销。 现代 CPU 通常有多核,每个核心还有自己的缓存。为了做到全局顺序一致,每次写入操作都必须同步给其他核心。为了减少性能开销,如果不需要全局顺序一致,我们应该考虑使用更加宽松的顺序模型。

3.2 memory_order_relaxed

最弱的内存顺序,它没有对操作的顺序或可见性做出任何保证。使用 memory_order_relaxed 意味着编译器和硬件可以对原子操作进行重排序,并且不需要保证其他线程立即看到修改后的值。这使得编译器和处理器有更大的优化空间,因为它们可以执行更多的指令重排和缓存优化,而无需考虑其他线程的影响。

memory_order_relaxed 可以用于 store,load 和 read-modify-write 操作,实现 relaxed 的顺序模型。这种模型下,只能保证操作的原子性和修改顺序 (modification order) 一致性,无法实现 synchronizes-with 的关系。

std::atomic<bool> x{false}, y{ false };

void thread1() {
    x.store(true, std::memory_order_relaxed); // (1)
    y.store(true, std::memory_order_relaxed); // (2)
}
void thread2() {
    while (!y.load(std::memory_order_relaxed)); // (3)
    assert(x.load()); // (4)
}
int main() {
    std::thread a(thread1), b(thread2);
    b.join(), a.join();
}

thread1 对不同的变量执行 store 操作。那么在某些线程看来,有可能是 x 先变为 true,y 后变为 true。另一些线程看来,又有可能是 y 先变为 true,x 后变为 true。

(4) 处的断言就有可能失败。因为 (2) 与 (3) 之间没有 synchronizes-with 的关系,所以就不能保证 (1) “happens-before” (4)。因此 (4) 就有可能读到 false。

Relaxed 顺序模型的开销很小。在 x86 架构下,memory_order_relaxed 的操作不会产生任何其他的指令,只会影响编译器优化,确保操作是原子的。

Relaxed 模型可以用在一些不需要线程同步的场景,例如 std::shared_ptr 增加引用计数时用的就是 memory_order_relaxed,因为不需要同步。但是减小应用计数不能用它,因为需要与析构操作同步。

3.3 acquire-release

Acquire(获取): 当一个线程执行一个原子操作,并使用 acquire 内存顺序时,它确保该操作之前的读操作能够在内存中完成,而后续的读写操作不能被重排序到该原子操作之前。这意味着该线程可以安全地读取其他线程写入的数据,并确保读取的数据是最新的。

Release(释放): 当一个线程执行一个原子操作,并使用 release 内存顺序时,它确保该操作后的写操作不能被重排序到该原子操作之前,而该原子操作之前的读写操作不能被重排序到该原子操作之后。这意味着该线程可以安全地将其写入的数据发布给其他线程,并确保其他线程能够看到该数据的最新值。

简单来说:任何指令都不能重排到 acquire 操作的前面,且不能重排到 release 操作的后面,否则会违反 acquire-release 的语义

在 acquire-release 模型中,会使用 memory_order_acquire,memory_order_release 和 memory_order_acq_rel 这三种内存顺序。它们的用法具体是这样的:

对原子变量的 load 可以使用 memory_order_acquire 内存顺序。这称为 acquire 操作。

对原子变量的 store 可以使用 memory_order_release 内存顺序。这称为 release 操作。

read-modify-write 操作即读 (load) 又写 (store),它可以使用 memory_order_acquire,memory_order_release 和 memory_order_acq_rel:

  • 如果使用 memory_order_acquire, 则作为 acquire 操作
  • 如果使用 memory_order_release, 则作为 release 操作
  • 如果使用 memory_order_acq_rel, 则同时为两者

Acquire-release 可以实现 synchronizes-with 的关系。如果一个 acquire 操作在同一个原子变量上读取到了一个 release 操作写入的值,则这个 release 操作 “synchronizes-with” 这个 acquire 操作。

std::atomic<bool> x{false}, y{false};

void thread1() {
    x.store(true, std::memory_order_relaxed); // (1)
    y.store(true, std::memory_order_release); // (2)
}

void thread2() {
    while (!y.load(std::memory_order_acquire)); // (3)
    assert(x.load(std::memory_order_relaxed)); // (4)
}

在上面的例子中,语句 (2) 使用 memory_order_release 在 y 中写入 true,语句 (3) 中使用 memory_order_acquire 从 y 中读取值。循环 (3) 退出时,它已经读取到了 y 的值为 true,也就是读取到了操作 (2) 中写入的值。因此有 (2) “synchronizes-with” (3)。

所以 (1) “happens-before” (4)。因此 (4) 能读取到 (1) 中写入的值,断言永远不会失败。即使 (1) 和 (4) 用的是 memory_order_relaxed。

3.1 节提到 sequencial consistent 模型可以实现 synchronizes-with 关系。事实上,内存顺序为 memory_order_seq_cst 的 load 操作和 store 操作可以分别视为 acquire 操作和 release 操作。因此对于两个指定了 memory_order_seq_cst 的 store 操作和 load 操作,如果后者读到了前者写入的值,则前者 “synchronizes-with” 后者。

为了实现 synchronizes-with 关系,acquire 操作和 release 操作应该成对出现。如果 memory_order_acquire 的 load 读到了 memory_order_relaxed 的 store 写入的值,或者 memory_order_relaxed 的 load 读到了 memory_order_release 的 store 写入的值,都不能实现 synchronizes-with 的关系。

虽然 sequencial consistent 模型能够像 acquire-release 一样实现同步,但是反过来 acquire-release 模型不能提供全局顺序一致性。如果将 3.1 节的例子中的 memory_order_seq_cst 换成 memory_order_acquire 和 memory_order_release:

void thread1() {
    x.store(true, std::memory_order_release); // (1)
}

void thread2() {
    y.store(true, std::memory_order_release); // (2)
}

void read_x_then_y() {
    while (!x.load(std::memory_order_acquire)); // (3)
    if (y.load(std::memory_order_acquire)) ++z; // (4)
}

void read_y_then_x() {
    while (!y.load(std::memory_order_acquire)); // (5)
    if (x.load(std::memory_order_acquire)) ++z; // (6)
}

则最终不能保证 z 不为 0。在同一次运行中, read_x_then_y 有可能看到先 (1) 后 (2),而 read_y_then_x 有可能看到先 (2) 后 (1)。这样有可能 (4) 和 (6) 的 load 的结果都为 false,导致最后 z 仍然为 0。

Acquire-release 的开销比 sequencial consistent 小。在 x86 架构下, memory_order_acquire 和 memory_order_release 的操作不会产生任何其他的指令,只会影响编译器的优化: 任何指令都不能重排到 acquire 操作的前面,且不能重排到 release 操作的后面,否则会违反 acquire-release 的语义。

因此很多需要实现 synchronizes-with 关系的场景都会使用 acquire-release。

3.4* Release sequences

到目前为止我们看到的, 无论是 sequencial consistent 还是 acquire-release,要想实现 synchronizes-with 的关系,acquire 操作必须在同一个原子变量上读到 release 操作的写入的值。如果 acquire 操作没有读到 release 操作写入的值,那么它俩之间通常没有 synchronizes-with 的关系。例如

std::atomic<int> x{0}, y{0};

void thread1() {
    x.store(1, std::memory_order_relaxed); // (1)
    y.store(1, std::memory_order_release); // (2)
}

void thread2() {
    y.store(2, std::memory_order_release); // (3)
}

void thread3() {
    while (!y.load(std::memory_order_acquire)); // (4)
    assert(x.load(std::memory_order_relaxed) == 1); // (5)
}

上面的例子中,只要 y 的值非 0 循环 (4) 就会退出。当它退出时,有可能读到 (2) 写入的值,也有可能读到 (3) 写入的值。如果是后者,则只能保证 (3) “synchronizes-with” (4),不能保证与 (2) 与 (4) 之间有同步关系。因此 (5) 处的断言就有可能失败。

但并不是只有在 acquire 操作读取到 release 操作写入的值时才能构成 synchronizes-with 关系。需要引入 release sequence 这个概念:

针对一个原子变量 M 的 release 操作 A 完成后,接下来 M 上可能还会有一连串的其他操作,如果这一连串操作是由

  • 同一线程上的写操作, 或者
  • 任意线程上的 read-modify-write 操作
    这两种构成的,则称这一连串的操作为以 release 操作 A 为首的 release sequence。这里的写操作和 read-modify-write 操作可以使用任意内存顺序。

如果一个 acquire 操作在同一个原子变量上读到了一个 release 操作写入的值,或者读到了以这个 release 操作为首的 release sequence 写入的值,那么这个 release 操作 “synchronizes-with” 这个 acquire 操作。如下例:

std::vector<int> data;
std::atomic<int> flag{0};

void thread1() {
    data.push_back(42); // (1)
    flag.store(1, std::memory_order_release); // (2)
}

void thread2() {
    int expected = 1;
    while (!flag.compare_exchange_strong(expected, 2, std::memory_order_relaxed)) // (3)
        expected = 1;
}

void thread3() {
    while (flag.load(std::memory_order_acquire) < 2); // (4)
    assert(data.at(0) == 42); // (5)
}

(3) 处的 compare_exchange_strong 是一种 read-modify-write 操作,它判断原子变量的值是否与期望的值 (第一个参数) 相等,如果相等则将原子变量设置成目标值 (第二个参数) 并返回 true,否则将第一个参数 (引用传递) 设置成原子变量当前值并返回 false。操作 (3) 会一直循环检查,当 flag 当值为 1 时,将其替换成 2。

所以 (3) 属于 (2) 的 release sequence。 而循环 (4) 退出时,它已经读到了 (3) 写入的值,也就是 release 操作 (2) 为首的 release sequence 写入的值。所以有 (2) “synchronizes-with” (4)。因此 (1) “happens-before” (5),(5) 处的断言不会失败。

注意 (3) 处的 compare_exchange_strong 的内存顺序是 memory_order_relaxed,所以 (2) 与 (3) 并不构成 synchronizes-with 的关系。也就是说, 当循环 (3) 退出时,并不能保证 thread2 能读到 data.at(0) 为 42。

但是 (3) 属于 (2) 的 release sequence,当 (4) 以 memory_order_acquire 的内存顺序读到 (2) 的 release sequence 写入的值时,可以与 (2) 构成 synchronizes-with 的关系。

3.5* memory_order_consume

memory_order_consume 其实是 acquire-release 模型的一部分,但是它比较特殊,它涉及到数据间相互依赖的关系。为此我们又要提出两个新概念: carries dependency(携带依赖) 和 dependency-ordered before(依赖顺序前)。

如果操作 a “sequenced-before” b,且 b 依赖 a 的数据,则 a “carries a dependency into” b。 一般来说,如果 a 的值用作 b 的一个操作数,或者 b 读取到了 a 写入的值,都可以称为 b 依赖于 a。例如

p++;  // (1)
i++;  // (2)
p[i]; // (3)

有 (1) “sequenced-before” (2) “sequenced-before” (3)。 (1) 和 (2) 的值作为 (3) 的下标运算符 [] 的操作数,所以有 (1) “carries a dependency into” (3) 和 (2) “carries a dependency into” (3)。但是 (1) 和 (2) 并没有相互依赖,它们之间没有 carries dependency 的关系。类似于 sequenced-before, carries dependency 关系具有传递性。

如果一个操作 A happens-before 另一个操作 B,并且 A 与 B 是依赖有序前驱关系,那么 A 对 B 具有 “dependency-ordered before” 的关系。这意味着在程序执行过程中,B 操作所依赖的数据将在 A 操作之后被 B 所观察到。

memory_order_consume 可以用于 load 操作。使用 memory_order_consume 的 load 称为 consume 操作。如果一个 consume 操作在同一个原子变量上读到了一个 release 操作写入的值,或以其为首的 release sequence 写入的值,则这个 release 操作 “dependency-ordered before” 这个 consume 操作。

std::atomic<std::string*> ptr;
int data;

void thread1() {
    std::string* p  = new std::string("Hello"); // (1)
    data = 42; // (2)
    ptr.store(p, std::memory_order_release); // (3)
}

void thread2() {
    std::string* p2;
    while (!(p2 = ptr.load(std::memory_order_consume))); // (4)
    assert(*p2 == "Hello"); // (5)
    assert(data == 42); // (6)
}

(4) 处的循环退出时,consume 操作 (4) 读取到 release 操作 (3) 写入的值,因此 (3) “dependency-ordered before” (4)

p2 的值作为 (5) 的操作数,因此 (4) “carries a dependency into” (5),也就是(4)数据依赖(5)。因为 (3)“dependency-ordered before” (4) 且 (4) “carries a dependency into” (5),所以 (3) “happens-before” (5)。

因为 (1) “sequenced-before” (3) 且 (3) “happens-before” (5), 所以 (1) “happens-before” (5)。所以 (1) “happens-before” (5)。

因此 (5) 可以读到 (1) 写入的值,断言 (5) 不会失败。但是操作 (6) 并不依赖于 (4),所以 (3) 和 (6) 之间没有 happens-before 的关系,因此断言 (6) 就有可能失败。

与 acquire-release 类似,在 x86 下使用 memory_order_consume 的操作不会产生任何其他的指令,只会影响编译器优化。与 consume 操作有依赖关系的指令都不会重排到 consume 操作前面。它对重排的限制比 acquire 宽松些,acquire 要求所有的指令都不能重排到它的前面,而 consume 只要求有依赖关系的指令不能重排到它的前面。因此在某些情况下,consume 的性能可能会高一些。

4. 案例(实现自旋锁)

在一些场景下,如果锁被占用的时间很短,我们会选择自旋锁,以减少上下文切换的开销。锁一般用来保护临界数据的读写,我们希望同一时间只有一个线程能获取到锁,且获取到锁后,被锁保护的数据总是最新的。前者通过原子操作即可保证,而后者就需要考虑内存顺序了。可以用 acquire-release 模型实现自旋锁。

class spinlock {
    std::atomic<bool> flag{false};
public:
    void lock() {
        while (flag.exchange(true, std::memory_order_acquire)); // (1)
    }
    void unlock() {
        flag.store(false, std::memory_order_release); // (2)
    }
};

spinlock mu;

void thread1() {
    // some operations
    mu.lock(); // (3)
}

void thread2() {
    mu.lock(); // (4)
}

(1) 处加锁用到的 exchange 是一种 read-modify-write 操作,它将目标值 (第一个参数) 写入原子变量,并返回写入前的值。在这个实现中,锁被占用时 flag 为 true。如果锁被占用,(1) 处的 exchange 操作会一直返回 true,线程阻塞在循环中,直到锁被释放。 flag 为 false,exchange 操作将 flag 重新置为 true 以抢占锁,并且返回其原来的值 false,循环退出,加锁成功。解锁则很简单, 将 flag 置为 false 即可。

由于解锁操作使用 memory_order_release 且加锁操作使用 memory_order_acquire,所以能保证加锁成功时与上一次解锁操作构成 “synchronizes-with” 的关系。

(3) 和 (4) 之间没有任何同步关系。假设先执行操作 (3) 后执行操作 (4),那么 thread1 中 (3) 之前的操作结果不一定对 thread2 可见。但只会有一个线程得到锁,这是由原子变量的修改顺序 (modification order) 所保证的。要么 thread1 先将 flag 置为 true,要么 thread2 先将 flag 置为 true,这个顺序是全局一致的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/554849.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

13、ESP32 深度睡眠

1、深度睡眠 ESP32 可以在不同的电源模式之间切换&#xff1a; 活动模式调制解调器睡眠模式浅睡眠模式深度睡眠模式休眠模式 在深度睡眠模式下&#xff0c;CPU 或 Wi-Fi 活动都不会发生&#xff0c;但超低功耗 &#xff08;ULP&#xff09; 协处理器仍可打开电源&#xff0c;R…

常见的数据结构

链表 链表&#xff1a;适用于插入删除多、读少的场景。 链表在新增、删除数据都比较容易&#xff0c;可以在 O(1) 的时间复杂度内完成。 但对于查找&#xff0c;不管是按照位置的查找还是按照数值条件的查找&#xff0c;都需要对全部数据进行遍历。这显然就是 O(n) 的时间复杂…

解锁流量密码:如何利用HubSpot打造高效的获客策略?(下)

在当今数字化时代&#xff0c;流量是企业成功的关键。HubSpot作为一款全面的营销自动化工具&#xff0c;为我们提供了强大的支持&#xff0c;帮助企业打造高效的流量获取策略。接下来&#xff0c;我们将从社交媒体与SEO优化、自动化营销流程、数据分析与效果评估以及流量获取策…

畅通工程(并查集)

//新生训练 #include <iostream> #include <algorithm> #include <bits/stdc.h> using namespace std;#define N 1010int fa[N];int Find(int x) {if (x ! fa[x]){fa[x] Find(fa[x]);}return fa[x]; }void Union(int x, int y) {int a Find(x);int b Find…

Linux读写文件

前言 学习了文件系统&#xff0c;就能理解为什么说Linux下一切皆文件。 语言层面的操作 在c语言的学习中我们可以使用fopen()函数对文件进行操作。 int main() {//FILE * fp fopen("./log.txt", "w");//FILE * fp fopen("./log.txt", "…

Compose 布局

文章目录 Compose 布局ColumnColumn属性使用 RowRow属性使用 BoxBox属性使用 ConstraintLayoutLazyColumnLazyColumn属性使用使用多类型使用粘性标题回到顶部 LazyRowLazyRow属性使用 LazyVerticalGridLazyVerticalGrid属性使用 Compose 布局 Column Compose中的”垂直线性布…

BlueNRG-X 原理图参数说明

1. 前言 为了让客户在原理图设计阶段少走弯路&#xff0c;我这里结合客户评估和设计阶段常遇到的问题&#xff0c;整理了一下 BlueNRG-1/-2 相关设计及注意事项以备客户解惑用。 2. BlueNRG-1/-2 的原理图参数说明及设计注意事项 2.1. BlueNRG-1/-2 原理图及参数如下&#x…

NFTScan | 04.08~04.14 NFT 市场热点汇总

欢迎来到由 NFT 基础设施 NFTScan 出品的 NFT 生态热点事件每周汇总。 周期&#xff1a;2024.04.08~ 2024.04.14 NFT Hot News 01/ 数据&#xff1a;Runestone 地板价突破 0.07 BTC&#xff0c;创历史新高 4 月 8 日&#xff0c;据数据显示&#xff0c;Runestone 地板价突破 …

【第七届openGauss技术文章征集】 openGauss新版本征文活动来啦!

活动背景 2024年3月30日&#xff0c;openGauss 6.0.0版本正式上线&#xff0c;该版本与之前版本特性功能保持兼容&#xff0c;在内核能力、DataPod三层资源池化架构、DataKit数据全生命周期管理平台、生态兼容性等方面全面增强。&#xff08;下方【点击原文】即可查看更多【新…

Android--ConnectivityManager使用

一、前言 Android10之后官方废弃了通过WifiManager连接WIFI的方式&#xff0c;现在要使用ConnectivityManager连接WIFI 二、连接WIFI public class MainActivity extends AppCompatActivity {private static final String TAG"lkx";Overrideprotected void onCrea…

【黑马头条】-day10热点文章定时计算-xxl-job

文章目录 1 今日内容1.1 需求分析1.2 解决方案1.3 定时计算1.4 定时任务方案对比 2 分布式任务调度3 xxl-job3.1 简介3.2 环境搭建3.2.1 配置maven3.2.2 源码说明 3.3 配置部署调度中心3.3.1 运行sql脚本3.3.2 修改配置application.properties3.3.3 启动引导类 3.4 docker配置x…

✌粤嵌—2024/4/18—旋转链表✌

代码实现&#xff1a; 方法一&#xff1a;在原链表中找到旋转之后的首尾&#xff0c;改变指向 /*** Definition for singly-linked list.* struct ListNode {* int val;* struct ListNode *next;* };*/ struct ListNode* rotateRight(struct ListNode *head, int k) {i…

电脑怎么创建加密磁盘?方法很简单!

在电脑中创建加密磁盘&#xff0c;可以有效地保护电脑数据&#xff0c;避免数据泄露。那么&#xff0c;电脑怎么创建加密磁盘呢&#xff1f;下面我们就一起来了解一下吧。 密盘创建方法 创建密盘建议使用超级秘密磁盘3000&#xff0c;软件界面简约&#xff0c;操作简单&#x…

秋招复习笔记——八股文部分:网络HTTP

常见面试题 基本概念 HTTP 是超文本传输协议&#xff0c;也就是HyperText Transfer Protocol。HTTP 协议是一个双向协议,是一个在计算机世界里专门用来在两点之间传输数据的约定和规范。是超越了普通文本的文本&#xff0c;它是文字、图片、视频等的混合体&#xff0c;最关键…

资源管理规范

踩坑经验&#xff1a; 对于IO及池化资源(文件、线程池、网络IO(HttpClient)、磁盘IO)&#xff0c;使用之后一定要及时回收&#xff0c;好借好还&#xff0c;再借不难。 稳定关闭方式 完成I/O操作后&#xff0c;应该关闭流以释放系统资源。可以使用finally块确保流被关闭&#…

学生故事|勇于创新,拒绝“一成不变”的设计

对于JIANG MANQI而言&#xff0c;室内设计一直是她钟爱的行业选择。 在没有进入莱佛士学习之前&#xff0c;MANQI受家人的影响&#xff0c;从小就对设计行业比较感兴趣。而她选择室内设计&#xff0c;是觉得室内设计是比较有前途的一个专业&#xff0c;随着人们生活品质的提高…

数字化转型-工具变量数据集

01、数据介绍 数字化转型是指企业或个人利用数字技术&#xff0c;如大数据、云计算、人工智能等&#xff0c;对其业务流程、运营模式、决策方式等进行全面、深入的变革&#xff0c;以提高效率、降低成本、提升质量、增强竞争力。在这个过程中&#xff0c;工具变量扮演着至关重…

SpringBoot 集成Nacos注册中心和配置中心-支持自动刷新配置

SpringBoot 集成Nacos注册中心和配置中心-支持自动刷新配置 本文介绍SpringBoot项目集成Nacos注册中心和配置中心的步骤&#xff0c;供各位参考使用 1、配置pom.xml 文件 在pom.xml文件中定义如下配置和引用依赖&#xff0c;如下所示&#xff1a; <properties><pr…

MATLAB求和函数

语法 S sum(A) S sum(A,“all”) S sum(A,dim) S sum(A,vecdim) S sum(,outtype) S sum(,nanflag) 说明 示例 S sum(A) 返回沿大小大于 1 的第一个数组维度计算的元素之和。 如果 A 是向量&#xff0c;则 sum(A) 返回元素之和。 如果 A 是矩阵&#xff0c;则 sum(A) 将…

如何在Linux CentOS部署宝塔面板并实现固定公网地址访问内网宝塔

文章目录 一、使用官网一键安装命令安装宝塔二、简单配置宝塔&#xff0c;内网穿透三、使用固定公网地址访问宝塔 宝塔面板作为建站运维工具&#xff0c;适合新手&#xff0c;简单好用。当我们在家里/公司搭建了宝塔&#xff0c;没有公网IP&#xff0c;但是想要在外也可以访问内…
最新文章