如题, x86_64处理器的指针赋值是原子操做吗?web
说实话我很讨厌参与讨论那些彷佛不肯定东西,倒不是说我对未知不敬畏,而是参与讨论的人大多数都是似懂非懂,对,我说的不肯定性指的是参与讨论的人的认知的不肯定,若是你本身都似懂非懂,那么我说什么你均可以反驳我,说些 “貌似,可能,并不绝对” 的词汇来让事情变得混乱。数组
最近写了一篇文章:
https://blog.csdn.net/dog250/article/details/103911008
里面提到了一句:
而后有人提出了质疑,其实我这句话后面还有个引用呢:
后面还夹杂了个人评价。bash
因此说,个人意思是, 以Intel的手册为准。 我以为我应该收回上面的那句 “which is a pointer assignment operation, on the x86_64 platform which is an atomic operation.” 而后从新说出个因此然来。网络
有了这段 权威描述 ,本文彷佛该结束了。布局
可是…性能
show me the code 是个金句,它鄙视的是 talk, 由于 talk is cheap.优化
那好吧,按照这样看来,上面那一段都是废话,很是cheap,那么下面就用代码说话吧。编码
声明一下:atom
如下无术语,也再也不引用任何cheap的东西。
本文虽然以“指针赋值”为例,但其实任何“int,long,long long”赋值均试用 。
我在代码里强转来强转去的,实际在暗示什么呢?CPU和内存不了解什么是指针,只有在执行的时候,一个unsigned long才会 变成 指针。
网络协议,序列化时,请 谨慎相信任何原子操做的承诺!
考虑一个指针p,两个或者多个线程分别对它进行赋值:
long *p;
// thread 1
p = a1;
// thread 2
p = a2
结果能够预期吗?若是你笃信指针赋值是原子操做,那么最终结果,p不是a1,即是a2,这是肯定的。
然而Intel手册里说,若是指针p跨越了cacheling的边界,便不能保证赋值操做是原子的,为了复现Intel的说法,从而证实指针赋值并不是原子的,只须要给出一个反例,即p既不是a1,也不是a2。
在编码以前,咱们先查一下本身实验的机器上的cacheline的大小:
root@zhaoya-VirtualBox:~/xdp/msvc# cat root@zhaoya-VirtualBox:~# cat /sys/devices/system/cpu/cpu1/cache/index0/coherency_line_size
64
OK,64字节的cacheline,咱们下面就制造一个跨越64字节边界的指针,直接看代码:
#include
#include
// 不让编译器自动扩充结构体对齐特征,这个在网络协议以及序列号操做中很常见。
#pragma pack(1)
struct pack {
// 60字节的padding
char unsued[60];
// cacheline仅剩下4字节,仅够装指针p的4字节,剩余4字节跨越到另外一个cacheline
long *p;
};
#pragma pack()
// 内存对齐粒度最小为1字节,我设置64个元素的数组,总有一个元素刚好是在64字节边界的!
struct pack pa[64];
// 保存64字节边界的元素
struct pack *used;
long a = 0x1122334455667788;
long b = 0x8877665544332211;
void *write_value1(void *param)
{
for (;;) {
used->p = (long *)a;
// 不让编译器优化
asm volatile("" ::: "memory");
}
return NULL;
}
void *write_value2(void *param)
{
for (;;) {
used->p =(long *)b;
asm volatile("" ::: "memory");
}
return NULL;
}
int main(int argc, char **argv)
{
int i;
long *p;
pthread_t t1, t2;
// for循环找到那个64字节边界的pack结构体元素
for (i = 0; i < 64; i++) {
unsigned long addr;
addr = (unsigned long)&pa[i];
if (addr%64 == 0) {
// 赋值给used
used = (struct pack *)addr;
break;
}
}
pthread_create(&t1, NULL, write_value1, NULL);
pthread_create(&t2, NULL, write_value2, NULL);
while (1) {
p = used->p;
// 咱们看能不能找到既不是a,又不是b的p
if (p != (long *)a && p != (long *)b) {
printf("%lx\n", (unsigned long)p);
}
}
return 0;
}
跑一下呗:
个人天!
这意味着什么?
这意味着不加锁的指针赋值,极其危险!
什么?难道编译器不帮忙?
编译器只能尽可能帮忙,哦,对了,还有Linux内核的伙伴系统,slab系统,都只是尽可能帮忙,其他的事,只能看造化。谁也不能保证你不会写出上面相似pack的结构体,所以结构体的字段布局很是重要。
即使这样,你也不能保证结构体对象刚好被载入你但愿的位置,只要超过一个cacheline大小的结构体,内部字段的赋值就必定当心再当心:
加锁影响性能
不加锁怕跨越边界
说白了这就是个手艺活。
若是你想快速复现指针跨越cacheline的赋值非原子性,直接加个修饰便可:
// 你也能够直接用aligned来固化地址对齐特征,然则不真实!
struct pack pa __attribute__ ((aligned(64)));
...
int main(...
...
used = &pa;
很少说了,深刻什么单条指令的原子执行,LOCK引脚,cache一致性原子操做粒度等等细节无益于解决实际问题,至于其它体系结构,摸都摸不到(我是说我本身),更显得纸上谈兵,到此为止了。
浙江温州皮鞋湿,下雨进水不会胖!