C++线程编程-内存顺序

news/2024/7/4 3:42:44

内存顺序模型

有六种内存顺序选项可以应用到原子类型上, memory_order_relaxed;memory_order_consume; memory_order_acquire;memory_order_release;memory_order_acq_rel; memory_order_seq_cst;

它们代表三种模型:

  1. 顺序一致 memory_order_seq_cst;
  2. 获得-释放:memory_order_consume; memory_order_acquire;memory_order_release;memory_order_acq_rel;
  3. 松散顺序:memory_order_relaxed;
    默认是memory_order_seq_cst;这是最严格的可用选项。

为什么要有内存顺序?

由于编译器对把代码重新打乱,排序,提前缓存给cpu去执行,以保证最优的执行效率,这个在单线程上是没有问题的,因为编译器会保证打乱后的代码的语意和打乱之前的一样;但是在并发编程中,这便有问题了:因为不同的CPU缓存和内部缓冲区,在同样的存储空间中可以存储不同的值。线程之间不会保证变量的时效性。

那么为什么要管这么多呢?不是有锁吗?内存顺序和原子操作的引入,是为了无锁的并发编程,提高并发编程的效率。所以就必须认为规定内存顺序,C++11引入了6种内存顺序。内存顺序可以理解为一个栅栏,在栅栏之前的一定比栅栏之后的先执行。

不同的内存顺序模型在不同的cpu架构上有不同的成本,它允许高手们利用更细粒度的顺序关系来提升性能, 在不太关键的情况下,当允许使用默认的顺序一致顺序时,他们是有优势的。

请教了大神:
编译器会对代码做乱序优化,cpu会对代码再进行乱序执行,但是所谓的乱序,其实是cpu和编译器认为这样更高效,而且没有副作用(它们认为没有副作用),所以帮我们做了一个这样的优化,在单线程上,这个乱序优化没有影响,因为就算是乱序,它们也会保证代码语义的正确性,所以不会说出现太离谱的行为;但是在多线程上,由于每个独立线程的内存对cpu来讲是不共享的,因为我们知道,在posix-thread环境下,就是深入内核态的线程中,我们使用的是内核态线程,内核态线程是cpu调度的基本单位(因为用户态的线程对cpu不可见,cpu仍然调度进程,比如windows线程),既然是cpu调度的基本单位,就会为每个线程单独保存上下文,缓冲区等等,这就有问题了:不同线程之间的缓冲区是不可见的(可以 用内存顺序可见,这里就先认为不可见),当两个线程之间的变量,有某种关系的依赖的时候,这种依赖就可能因为cpu的乱序执行而被破坏掉,因为在cpu看来,单独一个线程内,这些变量是没有依赖的,但实际上,多个线程之间是有依赖的,但是cpu看不见,就做了乱序优化,就导致了副作用。所以,我们使用内存顺序,要么就告诉编译器,这些指定的东西,就严格按照顺序执行,要么就按照 我指定的顺序执行,来防止副作用的产生。
另外,书上讲到的副作用,x86x64机器上是跑不出来的,因为是默认顺序执行的(没有那么大的乱序,或者说是默认sequentially consistent的),但是在ram中,可能就会有问题。

综上,基本可以说理解内存顺序,要理解两个东西:同一线程中,谁先执行,谁后执行;不同线程中,切换内存的时候会不会及时的把依赖量带过去,使另一个线程可见。

顺序一致顺序

默认的顺序: memory_order_seq_cst。

这意味着程序的行为与一个简单的顺序的世界观是一致的。如果所有原子类型实例上的操作是顺序一致的,多线程程序的行为,就好像是所有这些操作由单个线程以某种特定的顺序进行执行一样。

所有线程都必须看到操作的相同顺序。这也意味着,操作不能被重排。如果你的代码在一个线程中有一个操作在另一个之前,其顺序必须对所有其他的线程可见。

但是,它有代价,在一个带有许多处理器的弱顺序机器上,它可能导致显著的性能惩罚。

  1. 如果是读取就是 acquire 语义,如果是写入就是 release 语义,如果是读取+写入就是 acquire-release 语义
  2. 同时会对所有使用此 memory order 的原子操作进行同步,所有线程看到的内存操作的顺序都是一样的,就像单个线程在执行所有线程的指令一样

对顺序一致的理解就是:你看到的代码语义顺序,就是实际执行的顺序,并且别的线程在对应的顺序上可见。

// 顺序一致隐含着总体顺序
#include <atomic>
#include <thread>
#include <assert.h>
std::atomic<bool> x,y;
std::atomic<int> z;
void write_x()
{
	
	x.store(true,std::memory_order_seq_cst);	// 可以不写,这是默认
}
void write_y()
{
	y.store(true,std::memory_order_seq_cst);	// 可以不写,这是默认
}
void read_x_then_y()
{
	// 最先体现在这两句代码,while和if
	// 如果是顺序严格的,那么一定是先执行while,再执行if
	// 如果是顺序不严格的,那么就可能是先执行if,再执行while
	// 因为在cpu看来,while里面没有用到if里面的变量,就会认为它们之间是没有关系的,
	// 就会先给if装载值,然后等while执行完了,直接给if执行,而不会再去给if装在值,这就会导致问题,后面有例子。
	while(!x.load(std::memory_order_seq_cst));
	if(y.load(std::memory_order_seq_cst))
	{
		++z;
	}
}
void read_y_then_x()
{
	while(!y.load(std::memory_order_seq_cst));
	if(x.load(std::memory_order_seq_cst))
	{
		++z;
	}
}
int main()
{
	x = false;
	y = false;
	z = 0;
	std::thread a(write_x);
	std::thread b(write_y);
	std::thread c(read_x_then_y);
	std::thread d(read_y_then_x);
	a.join();
	b.join();
	c.join();
	d.join();
	std::cout << z << "\n";	// 要么==1,要么==2;

}

因为有顺序一致性,所以if中的装载不会重排到while的装载之前,所以这个z,要么是1,要么是2,不可能是0.

非顺序一致的内存顺序

如果不是上述的顺序,就有一种情况:事件不再有单一的全局顺序。
这意味着,不同的线程可能看到相同的操作的不同方面。 需要考虑线程的真正并行,而且线程不必和事件的顺序一致。这不仅仅意味着编译器能够重新排列指令。即使线程正在运行完全相同的代码,由于其他线程中的操作没有明确的顺序约束,它们可能与事件的顺序不一致,因为不同的CPU缓存和内部缓冲区可能为相同的内存保存了不同的值。
在没有其他的顺序约束时,唯一的要求时所有的线程 对每个独立变量的修改顺序达成一致。不同变量上的操作可以以不同的顺序出现在不同的线程中,前提是所能看到的值与所有附加的顺序约束是一致的。

为所有操作使用memory_order_relaxed,就能很好的展示这一全无序的操作。

松散顺序

内存顺序:memory_order_relaxed
原子类型不参与synchronizes-with关系,单线程中的同一个变量的操作仍然服从happens-before关系,但相对于其他线程的顺序几乎没有任何要求。

没有同步或顺序制约,仅对此操作要求原子性;只保证当前操作的原子性,不考虑线程间的同步,其他线程可能读到新值,也可能读到旧值。比如 C++ shared_ptr 里的引用计数,我们只关心当前的应用数量,而不关心谁在引用谁在解引用。

对松散顺序的理解:可能以任何顺序执行,可不可见也不一定。

// 线程 1 :
r1 = y.load(std::memory_order_relaxed); // A
x.store(r1, std::memory_order_relaxed); // B
// 线程 2 :
r2 = x.load(std::memory_order_relaxed); // C 
y.store(42, std::memory_order_relaxed); // D

允许产生结果r1 == 42 && r2 == 42;
因为即使线程 1 中 A 先序于 B 且线程 2 中 C 先序于 D ,
却没有制约避免 y 的修改顺序中 D 先出现于 A ,
而 x 的修改顺序中 B 先出现于 C 。 
D 在 y 上的副效应,可能可见于线程 1 中的加载 A ,同时 B 在 x 上的副效应,可能可见于线程 2 中的加载 C 。
// 放松操作有极少数的排序要求
#include <atomic>
#include <thread>
#include <assert.h>
#include <iostream>
std::atomic<bool> x,y;
std::atomic<int> z;
void write_x_then_y()
{
	// 可能是x先存储,也可能是y先存储,所以z可能是0,如果y先存储,x还没存储的时候
    x.store(true,std::memory_order_relaxed);
    y.store(true,std::memory_order_relaxed); 
}
void read_y_then_x()
{
    while(!y.load(std::memory_order_relaxed));
    if(x.load(std::memory_order_relaxed))
    {
        ++z;
    }
}
int main()
{
    x = false;
    y = false;
    z = 0;
    std::thread a(write_x_then_y);
    std::thread b(read_y_then_x);
    a.join();
    b.join();
    std::cout << z << "\n";
    assert(z.load()!=0);
    exit(0);
}

上述情况z可以是0,原因不必说了,上面提到过,完全松散的顺序,就有可能会使得if提前装载,while之后装载,导致while刚开始执行的时候,if就是false了。

稍微复杂的例子:

#include <thread>
#include <atomic>
#include <iostream>
std::atomic<int> x(0), y(0), z(0);
std::atomic<bool> go(false);
const unsigned loop_count = 10;
struct read_values
{
    int x, y, z;
};
read_values values1[loop_count];
read_values values2[loop_count];
read_values values3[loop_count];
read_values values4[loop_count];
read_values values5[loop_count];

void increment(std::atomic<int> *var_to_inc, read_values *values)
{
    while (!go)
    {
        std::this_thread::yield(); // 出让调度器
    }
    for (unsigned i = 0; i < loop_count; ++i)
    {
        values[i].x = x.load(std::memory_order_relaxed);
        values[i].y = y.load(std::memory_order_relaxed);
        values[i].z = z.load(std::memory_order_relaxed);
        var_to_inc->store(i + 1, std::memory_order_relaxed);
        std::this_thread::yield();
    }
}

void read_vals(read_values *values)
{
    while (!go)
    {
        std::this_thread::yield(); // 出让调度器
    }
    for (unsigned i = 0; i < loop_count; ++i)
    {
        values[i].x = x.load(std::memory_order_relaxed);
        values[i].y = y.load(std::memory_order_relaxed);
        values[i].z = z.load(std::memory_order_relaxed);
        std::this_thread::yield();
    }
}

void print(read_values *v)
{
    for (unsigned i = 0; i < loop_count; ++i)
    {
        if (i)
        {
            std::cout << ",";
        }
        std::cout << "(" << v[i].x << "," << v[i].y << "," << v[i].z << ")";
    }
    std::cout << "\n\n";
}

int main()
{
    std::thread t1(increment, &x, values1);
    std::thread t2(increment, &y, values2);
    std::thread t3(increment, &z, values3);
    std::thread t4(read_vals, values4);
    std::thread t5(read_vals, values5);
    // 使用go仅仅是因为想让线程同时开始运行
    go = true;
    t5.join();
    t4.join();
    t3.join();
    t2.join();
    t1.join();
    print(values1);
    print(values2);  
    print(values3);
    print(values4);
    print(values5);
}

强烈建议避免松散的原子操作,除非绝对必要,即便这样,也应该极其谨慎的使用;它们应该与具有更强的顺序语义的原子操作结合起来使用,以便在线程同步中发挥作用。

获取-释放顺序

获取-释放顺序是松散顺序的进步,操作仍然没有总的顺序,但的确引入了一些同步。
在这种顺序模型下:原子载入是获取操作(memory_order_acquire),原子存储时释放操作(memory_order_release),原子的读-修改-写操作(如fetch_add() 和 exchange())是获取、释放或两者兼备(memory_order_acq_rcl)。

若线程 A 中的一个原子存储带标签 memory_order_release ,而线程 B 中来自同一变量的原子加载带标签memory_order_acquire ,则从线程 A 的视角先发生于原子存储的所有内存写入(非原子及宽松原子的),在线程 B 中成为可见副效应,即一旦原子加载完成,则保证线程 B 能观察到线程 A 写入内存的所有内容。同步仅建立在释放和获得同一原子对象的线程之间。其他线程可能看到与被同步线程的一者或两者相异的内存访问顺序。

在强顺序系统( x86 、 SPARC TSO 、 IBM 主框架)上,释放获得顺序对于多数操作是自动进行的。无需为此同步模式添加额外的 CPU 指令,只有某些编译器优化受影响(例如,编译器被禁止将非原子存储移到原子存储-释放后,或将非原子加载移到原子加载-获得前)。在弱顺序系统( ARM 、 Itanium 、 Power PC )上,必须使用特别的 CPU 加载或内存栅栏指令。

// 获取-释放并不意味着总体排序
#include <atomic>
#include <thread>
#include <assert.h>
#include <iostream>
std::atomic<bool> x,y;
std::atomic<int> z;
//a
void write_x()
{
	x.store(true,std::memory_order_release);	// 1
}
//b
void write_y()
{
	y.store(true,std::memory_order_release);	// 2
}
//c
void read_x_then_y()
{
	while(!x.load(std::memory_order_acquire));	// 3
	if(y.load(std::memory_order_acquire))		// 4
	{
		++z;
	}
}
//d
void read_y_then_x()
{
	while(!y.load(std::memory_order_acquire));	// 5
	if(x.load(std::memory_order_acquire))		// 6
	{
		++z;
	}
}
int main()
{
	x = false;
	y = false;
	z = 0;
	std::thread a(write_x);
	std::thread b(write_y);
	std::thread c(read_x_then_y);
	std::thread d(read_y_then_x);
	a.join();
	b.join();
	c.join();
	d.join();
    std::cout << z << "\n";
    assert(z.load()!=0);
}

可以看到store是用了释放操作release,load使用了获取操作acquire。断言可能触发,因为对于4和6,都是有可能读到false的,那说明什么?acquire规定,当前线程中读或写不能被重排到此加载前,也就是说,6不可能到5之前,4不可能到3之前,那是怎么两个都读到false的?我感觉,是这个例子有错误?
非也,官方文档有这么一句话:同步仅建立在释放和获得同一原子对象的线程之间。其他线程可能看到与被同步线程的一者或两者相异的内存访问顺序。
也就是说,对于a线程来说,x的store操作,对于c和d线程中的x的load的操作是同步的,反之亦然,就是说,从a线程来看,如果c线程的load执行了,那么c的load执行之后的操作绝对不会在c的之前执行;从c线程看a线程,如果a的store执行了,那么a的store之前的读写操作也一定不会跑到a的store操作之后执行。而从b线程看c和d线程中的x是怎么样的,完全都有可能,因为b的y操作和cd线程的x操作根本就不同步。也就是说,x和y因为是由不同线程写入的,所以每种情况下从释放到获取的顺序对于另一个线程中的操作是没有影响的。
在程序加载的时候,如果x和y的存储没有顺序关系(比如这个例子),那么它们的加载就没有顺序关系,即使用了内存顺序。如果存储有对应的顺序,那么加载就也有对应的顺序。

另一个例子:

#include <atomic>
#include <thread>
#include <assert.h>
#include <iostream>
std::atomic<bool> x,y;
std::atomic<int> z;

void write_x_then_y()
{
	// x和y的加载有顺序关系,并且x必须在y之前完成加载
	x.store(true,std::memory_order_relaxed);	// 1
	y.store(true,std::memory_order_release);	// 2
}
void read_y_then_x()
{
	// 程序装载的时候,因为x和y的store有顺序,所以x和y的load也会被规定顺序
	// 这里指定acquire,所以y必须在x前面加载
	while(!y.load(std::memory_order_acquire));	// 3
	if(x.load(std::memory_order_relaxed))		// 4
	{
		++z;
	}
}
int main()
{
	x = false;
	y = false;
	z = 0;
	std::thread c(write_x_then_y);
	std::thread d(read_y_then_x);
	c.join();
	d.join();
    std::cout << z << "\n";
    assert(z.load() != 0);
}

断言不可能触发,z不可能等于零,1发生在2之前,因为它们在同一个线程里,1发生在3之前,所以1发生在4之前。

memory_order_consume

有此内存顺序的加载操作,在其影响的内存位置进行消费操作:当前线程中依赖于当前加载的该值的读或写不能被重排到此加载前。其他释放同一原子变量的线程的对数据依赖变量的写入,为当前线程所可见。在大多数平台上,这只影响到编译器优化。

若线程 A 中的原子存储带标签 memory_order_release 而线程 B 中来自同一对象的读取存储值的原子加载带标签 memory_order_consume ,则线程 A 视角中先发生于原子存储的所有内存写入(非原子和宽松原子的),会在线程 B 中该加载操作所携带依赖进入的操作中变成可见副效应,即一旦完成原子加载,则保证线程B中,使用从该加载获得的值的运算符和函数,能见到线程 A 写入内存的内容。

可以理解为:线程A中带release的store 操作的所有之前依赖的写操作(必须是和这个store有依赖的),对B线程中的带有consume的load的操作的值是可见的。
同步仅在释放和消费同一原子对象的线程间建立。其他线程能见到与被同步线程的一者或两者相异的内存访问顺序。

例子:

#include <atomic>
#include <iostream>
#include <assert.h>
#include <thread>
#include <string>
#include <chrono>
struct X
{
	int i;
	std::string s;
};
std::atomic<X*> p;
std::atomic<int> a;

void create_x()
{
	X *x = new X;
	x->i = 42;
	x->s = "hello";
	// a一定在p之前执行,但是a不一定对use_x可见。
	// 因为加载内存的时候,不一定会把a带过去
	a.store(99,std::memory_order_relaxed);	
	p.store(x,std::memory_order_release);
}

void use_x()
{
	X *x;
	while(!(x=p.load(std::memory_order_consume)))
	{
		// 休眠1ms
		std::this_thread::sleep_for(std::chrono::microseconds(1));
	}
	// 这三个断言只有a会发生断言,因为虽然p声明为release,但是p并不依赖于a,所以a没有被顺序标记,
	// cpu会认为a和p没有关系,所以切换内存的时候,会把p的依赖带回来,但是不一定会把a带回来。
	assert(x->i == 42);
	assert(x->s == "hello");
	assert(a.load(std::memory_order_relaxed) == 99);
}

int main()
{
	std::thread t1(create_x);
	std::thread t2(use_x);
	t1.join();
	t2.join();
}

顺序解除

如果你想让编译器能够在寄存器中缓存值,并且对操作进行重新排序以优化代码,而不用担心依赖,可以使用:std::kill_dependency();显示打破依赖链条。
例子:

int gloabl_data[] = {...};
std::atomic<int> index;
void f()
{
	int i = index.load(std::memory_order_consume);
	do_something_with(gloabl_data[std::dependency(i)]);
}

释放序列

// 释放序列
// 使用原子操作从队列中读取值
#include <atomic>
#include <thread>
#include <vector>
std::vector<int> queue_data;
std::atomic<int> count;

void populate_queue()
{
    unsigned const number_of_tiems = 20;
    queue_data.clear();
    for(unsigned i = 0; i < number_of_tiems; ++i)
    {
        queue_data.push_back(i);
    }
    count.store(number_of_tiems,std::memory_order_release); // 最初的存储
}

void consume_queue_items()
{
    while (true)
    {
        int item_index;
        if((item_index = count.fetch_sub(1,std::memory_order_acquire)) <= 0)
        {
            // wait_for_more_items();
            continue;
        }
        // process(queue_data[item_index-1]);
    }
}
int main()
{
    std::thread a(populate_queue);
    std::thread b(consume_queue_items);
    std::thread c(consume_queue_items);
    a.join();
    b.join();
    c.join();
}

看上面程序,如果只有一个消费者线程,是良好的,fetch_sub()是一个具有acquire语义的读取,并且存储是release的,两者的存储和载入同步,并且可见。但是如果有两个消费者线程在读,第二个fetch_sub()就会看到第一个fetch_sub()写下的值,而不是由store写下的值,**如果没有释放序列的规则,**第二个线程和第一个线程就没有happens-before关系,并且读取共享缓冲区也不是安全的,除非第一个fetch_sub() 也具有release语义,但这会带来消费者和生产者之间不必要的同步。如果fetch_sub()没有释放序列规则,或者是release的,就没有什么能要求对queue_data的存储对第二个消费者可见,就会遇到数据竞争。不过第一个fetch_sub()参与了释放序列。

如果存储被标记为memory_order_release , memory_order_acq_rel或memory_order_seq_cst,载入被标记为memory_order_consume、memory_order_acquire或memory_order_seq_cst,并且链条中的每个操作都载入由之前操作写入的值,那么,该操作链条就构成了一个释放序列( release sequence )

屏障

没有一套屏障的原子操作库是不完整的。这些操作可以强制内存顺序约束,而无需修改任何数据,并且与使用memory_order_relaxed顺序约束的原子操作组合起来使用。屏障是全局操作,能在执行该屏障的线程里影响其他原子操作的顺序。屏障一般也被称为内存障碍(memory barriers),它们之所以这样命名,是因为它们在代码中放置了一行代码,使得特定的操作无法穿越;在独立变量上的松散操作通常可以自由地被编译器或硬件重新排序。屏障限制了这一自由,并且在之前并不存在的地方引入happens-before和 synchronizes-with关系。

// 松散操作可以使用屏障来排序
// 内存障碍
#include <atomic>
#include <thread>
#include <assert.h>

std::atomic<bool> x,y;
std::atomic<int> z;

// A
void write_x_then_y()
{
    x.store(true,std::memory_order_relaxed);				// 1
    std::atomic_thread_fence(std::memory_order_release);	// FA
    y.store(true,std::memory_order_relaxed);				// X
}
// B
void read_y_then_x()
{
    while(!y.load(std::memory_order_relaxed));				// Y
    std::atomic_thread_fence(std::memory_order_acquire);	// FB
    if(x.load(std::memory_order_relaxed))					// 6
    {
        ++z;
    }
}

int main()
{
    x = false;
    y = false;
    z = 0;
    std::thread a(write_x_then_y);
    std::thread b(read_y_then_x);
    a.join();
    b.join();
    assert(z.load() != 0);
}

栅栏原子同步
线程 A 中的释放栅栏 F 同步于线程 B 中的原子获得操作 Y ,若

存在原子存储 X (带任何内存顺序)
Y 读取 X 所写入的值(或会为 X 所引领的释放序列所写入的值,若 X 是释放操作)
F 在线程 A 中先序于 X
此情况下,所有线程 A 中先序于 F 的非原子和宽松原子存储将先发生于线程 B 中所有 Y 后的,来自同一位置的非原子和宽松原子加载。

原子栅栏同步
线程 A 中的原子释放操作 X 同步于线程 B 中的获得栅栏 F ,若

存在原子读取 Y (带任何内存顺序)
Y 读取 X (或 X 所引领的释放序列)所写入的值
Y 在线程 B 中先序于 F
此情况下,线程 A 中所有先序于 X 的非原子和宽松原子存储,将先发生于线程 B 中所有 F 后的,来自同一位置的非原子和宽松原子加载。

栅栏栅栏同步
线程 A 中的释放栅栏 FA 同步于线程 B 中的获得栅栏 FB ,若

存在原子对象 M ,
线程 A 中存在修改 M 的原子写入 X (带任何内存顺序)
线程 A 中 FA 先序于 X
线程 B 中存在原子读取 Y (带任何内存顺序)
Y 读取 X 所写入的值(或会为 X 所引领的释放序列所写入的值,若 X 是释放操作)
线程 B 中 Y 先序于 FB
此情况下,线程 A 中所有先序于 FA 的非原子和宽松原子存储,将先发生于线程 B 中所有 FB 后的,来自同一位置的非原子和宽松原子加载。

这个演示,解释 栅栏栅栏同步。
根据例子,1一定会先发生于6,所以断言一定不会触发。发生的顺序是 1 -> FA~FB -> 6
释放屏障FA的效果,于对y的存储X被标记为memory_order_release是相似的,获取屏障FB,令其从y的载入Y被标记为memory_order_acquire是相似的。

这是屏障的总体思路:如果获取操作看到了释放屏障后发生的存储的结果,该屏障与获取操作同步;如果在获取屏障之前发生的载入看到释放操作的结果,该释放操作与获取屏障同步。当然,你可以在两边都设置屏障,就像在这里的例子一样,在这种情况下,如果在获取屏障之前发生的载入看见了释放屏障之后发生的存储所写下的值,该释放屏障与获取屏障同步。

尽管屏障同步依赖于在屏障之前或之后的操作所读取或写入的值,但是同步点仍是屏障本事。

如果把x改成普通变量:

// 内存障碍
#include <atomic>
#include <thread>
#include <assert.h>

bool x = false; // 非原子变量
std::atomic<bool> y;
std::atomic<int> z;

void write_x_then_y()
{
    x = true;
    std::atomic_thread_fence(std::memory_order_release);
    y.store(true,std::memory_order_relaxed);
}

void read_y_then_x()
{
    while(!y.load(std::memory_order_relaxed));
    std::atomic_thread_fence(std::memory_order_acquire);
    if(x)
    {
        ++z;
    }
}


int main()
{
    y = false;
    z = 0;
    std::thread a(write_x_then_y);
    std::thread b(read_y_then_x);
    a.join();
    b.join();
    assert(z.load() != 0);// 不会触发
}

屏障仍然为 对x的存储,对y的存储,从y的载入和从x的载入提供了强制顺序,并且在对x 的存储和从x 的载入之间仍然又happens-before关系,所以断言还是不会触发。

存储和载入y仍然必须是原子的,因为两个屏障之间的内容是没有顺序的。所以如果不是原子的话,可能会发生数据竞争。


http://www.niftyadmin.cn/n/3459256.html

相关文章

看源码,我为什么推荐IDEA?

1.条件断点 看源码的时候,经常遇到这个情况,源码中有个for循环,关键是这个list的size有时候长达数百个.但是我们只想debug一种情况.肥朝就曾经见过,在for循环中打了断点,一直按跳过,按了数十下之后.才找到自己想debug的值.这样效率不高 比如下文这个 1Test2public void testLis…

SQLServer中SYSCOLUMNS表的各个字段的意义

列名 数据类型 描述 name sysname 列名或过程参数的名称。 id int 该列所属的表对象 ID&#xff0c;或与该参数关联的存储过程 ID。 xtype tinyint systypes 中的物理存储类型。 typestat tinyint 仅限内部使用。 xusertype smallint 扩展的用户定义…

C++线程编程-设计基于锁的并发数据结构

序列化 多个线程轮流存取互斥元保护的数据&#xff0c;它们必须线性的而非并发的存取数据。 高并发就意味着&#xff1a;更小的保护区域&#xff0c;更少的序列化&#xff0c;更高的并发潜能。 设计基于锁的并发数据结构关键是要确保存取数据时要锁住正确的互斥元&#xff0c…

SQLServer常用系统存储过程

sp_add_log_file_recover_suspect_lib 当数据库的复原不能完成时,向文件组增加一个日志文件sp_add_targetservergroup 增家指定的服务器组sp_add_targetsvrgrp_member 在指定的目标服务器组增加一个目标服务器sp_addapprole 在数据库里增加一个特殊的应用程…

深入理解Java中的底层阻塞原理及实现

Information Technology Solutions as a Presentation 谈到阻塞&#xff0c;相信大家都不会陌生了。阻塞的应用场景真的多得不要不要的&#xff0c;比如 生产-消费模式&#xff0c;限流统计等等。什么 ArrayBlockingQueue、 LinkedBlockingQueue、DelayQueue 等等&#xff0c;都…

sqlserver 所有系统和用户定义错误消息

下面的示例将查询 sys.messages 目录视图以返回具有英文文本 (1033) 的数据库引擎中所有系统和用户定义错误消息的列表。 language_id1033 英文 language_id1041 日文 language_id2052 中文简体 language_id1028 中文繁体 SELECTmessage_id,language_id,severity,is_event_log…

DDD学习笔记 - 领域、子域和限界上下文

在设计欠佳的软件里&#xff0c;子域和限界上下文(context)很难存在一对一的映射关系。-- 说明设计较好的&#xff0c;子域和限界上下文应该是一对一的映射关系。 在不同的模型中存在名字相同或者相近的对象&#xff0c;但是它们的意思缺不同。当模型被一个显示的边界所包围时&…

C++线程编程-设计无锁的并发数据结构

定义和结果 使用互斥元、条件变量以及future 来同步数据的算法和数据结构被称为阻塞的算法和数据结构.调用库函数的应用会中断一个线程的执行&#xff0c;直到另一个线程执行一个动作.这种库函数调用被称为阻塞调用&#xff0c;因为直到阻塞被释放时线程才能继续执行下去.通常…