并行编程之多线程共享非volatile变量,会不会可能导致线程while死循环
背景
大家都知道线程之间共享变量要用volatilekeyword。可是,假设不用volatile来标识,会不会导致线程死循环?比方以下的伪代码:
static int flag = -1;
void thread1(){
while(flag > 0){
//wait or do something
}
}
void thread2(){
//do something
flag = -1;
}
线程1,线程2同一时候执行,线程2退出之后,线程1会不会有可能由于缓存等原因,一直死循环?
真实的世界
第一个坑:不靠谱的编绎器
直接上代码:
#include <pthread.h>
#include <unistd.h>
#include <stdio.h> static int vvv = 1;
void* thread1(void *){
sleep(2);
printf("sss\n");
vvv = -1;
return NULL;
}
int main() {
pthread_t t;
int re = pthread_create(&t, NULL, &thread1, NULL);
if(re < 0){
perror("thread");
}
while(vvv > 0){
// sleep(1);
}
return 0;
}
在main函数里启动了一个线程thread1,thread1会等待一段时间后改动vvv = -1,然后当vvv > 0时,主线程会一直while循环等待。
理想的情况下是这种:
主线程死循环等待,2秒之后thread1输出"sss",thread1退出,主线程退出。
保存为thread-study.c 文件,直接用gcc -O3 优化:
gcc thread-study.c -O3 -pthread -gstabs
再运行 ./a.out,能够发现控制台输出“sss”之后,会一直等待,再查看CPU使用率,一个核跑满了,说明主线程在死循环。
貌似就像上面所的,主线程由于缓存的原因,导致读取的 vvv 变量一直是旧的,从而死循环了。
可是否真的如此?
经过測试,除了O0级别(即全然不优化)不死循环外,O1,O2,O3级别,都会死循环。
再查看下O3级别的汇编代码(用 gcc -S thread-study.c 生成),main函数部分是这种:
为了便于查看,手动加了凝视。
main:
.LFB56:
.cfi_startproc
subq $24, %rsp
.cfi_def_cfa_offset 32
xorl %ecx, %ecx
xorl %esi, %esi
movl $_Z7thread1Pv, %edx
movq %rsp, %rdi
call pthread_create //int re = pthread_create(&t, NULL, &thread1, NULL);
testl %eax, %eax
js .L9
.L4:
movl _ZL3vvv(%rip), %eax //while(vvv > 0){
testl %eax, %eax
jle .L5
<strong>.L6:
jmp .L6</strong>
.p2align 4,,10
.p2align 3
.L5:
xorl %eax, %eax
addq $24, %rsp
.cfi_remember_state
.cfi_def_cfa_offset 8
ret
.L9:
.cfi_restore_state
movl $.LC1, %edi
call perror //perror("thread");
jmp .L4
.cfi_endproc
在L6标号那里,比較奇怪:
.L6:
jmp .L6
这里明显就是死循环,根本没有去尝试读取xxx的值。那么L4那个标号又是怎么回事?L4的代码是读取 vvv 变量再推断。可是它为什么没有在循环里?
再用gdb从汇编调试下,发现主线程的确是运行了死循环:
0x0000000000400609 <+25>: mov 0x200a51(%rip),%eax # 0x601060 <_ZL3vvv>
0x000000000040060f <+31>: test %eax,%eax
0x0000000000400611 <+33>: jle 0x400618 <main+40>
<strong>=> 0x0000000000400613 <+35>: jmp 0x400613 <main+35></strong>
0x0000000000400615 <+37>: nopl (%rax)
一个jmp指令原地跳转,自然是一个死循环,正相应上面汇编代码的L6部分。
相当于生成了这种代码:
if(vvv > 0){
goto return
}
for(;;){
}
可见gcc生成的代码有问题,它根本就没有生成正确的汇编代码。虽然这样的优化是符合规范的,但我个人比較反感这样的严重违反直觉的优化。
那么我们的问题还没有解决,接下来改动汇编代码,让它真正的像这样所预期的那样工作。仅仅要简单地把L6的jmp跳转到L4上:
.L4:
movl _ZL3vvv(%rip), %eax
testl %eax, %eax
jle .L5
.L6:
jmp .L4
.p2align 4,,10
.p2align 3
这个才我们真正预期的代码。
再測试下这个改动过后的代码:
gcc thread-study.s -o test -pthread -gstabs -O3
./test
运行2秒之后,退出了。
说明,主线程并没有一直读取到旧的共享变量的值,符合预期。
加上volatile
给" vvv "变量加上volatile,即:
volatile static int vvv = 1;
又一次编绎后,再跑下,发现正常了,2秒后进程退出。
查看下汇编代码,是这种:
.L5:
movl _ZL3vvv(%rip), %eax
testl %eax, %eax
setg %al
testb %al, %al
jne .L5
这段汇编代码符合预期。
可是这里还是有点不正确,volatile的特殊性在哪里?生成的汇编没有什么特别的指令,那它是怎样“防止”了线程不缓存共享变量的?
网上流传的一种说法是使用volatilekeyword之后,读取数据一定从内存中读取。
这样的说法既是对的,也是错的。volatilekeyword防止了编绎器优化,所以对于变量不会被放到寄存器里,或者被优化掉。可是volatile并不能防止CPU从Cache中读取数据。
所谓的“缓存”究竟是什么
CPU内部有寄存器,有各级Cache,L1,L2,L3。我们来考虑下究竟如何才会出现线程共享变量被放到CPU的寄存器或者各级Cache的情况。
volatile阻止了编绎器把变量放到寄存器里,那么对线程共享变量的读取即直接的内存訪问。
CPU Cache
CPU Cache放的正是内存的数据,像
movl _ZL3vvv(%rip), %eax
这种指令,是会先从CPU Cache里查找,假设没有的话,再通过总线到内存里读取。
而现代CPU有多核,通常来说每一个核的L1, L2 Cache是不共享的,L3 Cache是共享的。
那么问题就变成了:线程A改动了Cache中的内容,线程B是否会一直读取到的都是旧数据?
MESI协议
既然Cache数据会不一致,那么自然要有个机制,让它们之间重回一致。经典的Cache一致性协议是MESI协议。
MESI协议是使用的是Write Back策略,即当一个核内的Cache更新了,它仅仅改动自己核内部的,并非同步改动到其他核上。
在MESI协议里,每行Cache Line能够有4种状态:
- Modified 该Cache Line数据被改动,和内存中的不一致,数据仅仅存储在本Cache Line里。
- Exclusive 该Cache Line数据和内存中的一致,数据仅仅存在本Cache Line里。
- Shared 该Cache Line数据和内存中的一致,数据存在多个Cache Line里,随时会变成Invalid状态。
- Invalid 该Cache Line数据无效(即不会再使用)
MESI协议里,状态的转换比較复杂,可是都和人的直觉一致。对于我们研究的问题而言,仅仅须要知道:
当是Shared状态的时,改动Cache Line的内容前,要先通过Request For Ownership (RFO)的方式广播通知其他核,把Cache Line置为Invalid。
当是Modified状态时,Cache控制器会(snoop)拦截其他核对该Cache Line相应的内存地址的訪问,在回应回插入当前Cache Line的数据。并把本Cache Line的内容回写到内存里,状态改为Shared。
因此,并不会存在一个核内的Cache数据改动了,还有一个核没有感知的情况。
即不会出现线程A改动了Cache中的内容,线程B一直读取到的都是旧数据的情况。考虑到CPU内部通迅都是非常快的,本人预计线程A改动了共享变量,线程B读取到新值的时间应该是纳秒级之内。
另一个坑:CPU乱序运行
现代非常多CPU都有乱序运行能力,从上面加了volatile之后生成的汇编代码来看,没有什么特别的地方。那么它对于CPU乱序运行也是无能为力的。比方:
volatile static int flag = -1;
void thread1(){
...
jobA();
flag = 1;
}
void thread2(){
...
while(1){
if(flag > 0)
jobB();
}
}
对于这两个线程,jobB()有可能比jobA()先运行!
由于thread1里,可能会由于CPU乱序运行,先运行了flag = 1,再运行jobA()。
那么怎样防止这样的情况?这个麻烦是CPU搞出来的,自然也是CPU提供的解决的方法。
GCC内置了一些原子内存訪问的函数,如:
http://gcc.gnu.org/onlinedocs/gcc-4.6.2/gcc/Atomic-Builtins.html
type __sync_fetch_and_add (type *ptr, type value, ...)
type __sync_fetch_and_sub (type *ptr, type value, ...)
type __sync_fetch_and_or (type *ptr, type value, ...)
type __sync_fetch_and_and (type *ptr, type value, ...)
type __sync_fetch_and_xor (type *ptr, type value, ...)
type __sync_fetch_and_nand (type *ptr, type value, ...)
这些函数实际即隐含了memory barrier。
比方为之前讨论的代码加上memory barrier:
while(true){
__sync_fetch_and_add(&vvv,0);
if(vvv < 0 )
break;
}
再查看下生成的汇编代码:
.L4:
<strong>lock addl $0, _ZL3vvv(%rip)</strong>
movl _ZL3vvv(%rip), %eax
shrl $31, %eax
testb %al, %al
je .L5
jmp .L8
.L5:
jmp .L4
能够看到,加多了一条 lock addl 的指令。
这个lock,实际上是一个指令前缀,它保证了当前操作的Cache Line是处于Exclusive状态,并且保证了指令的顺序性。这个指令有可能是通过锁总线来实现的,可是假设总线已经被锁住了,那么仅仅会消耗后缀指令的时间。
实际上Java里的volatile就是在前面加了一个lock add指令实现的。这个有空再写。
其他的一些东东
有些场景能够不用volatile
抛开上面的讨论,事实上有些场景能够不使用volatile,比方这样的随机获取资源的代码:
ramdonArray[10];
int pos = 0;
Resource getResource(){
return ramdonArray[pos++%10];
}
这种代码pos是非volatile,但多线程调用getResource()函数全然没有问题。
C11与C++11
为什么C11和C++11不把volatile升级为java/C#那样的语义?我猜可能是所谓的“兼容性”问题。。蛋疼
C++11提供了Atomic相关的操作,语义和Java里的volatile差点儿相同。可是C11仍然没有什么好的办法,貌似仅仅能用GCC内置函数,或者写一些类似的汇编的宏了。
http://en.cppreference.com/w/cpp/atomic
GCC优化的一些东东
事实上在讨论的代码里,假设while循环里多一些代码,GCC可能就分辨不出能否优化了
优化的一些东东:
比方,在大部分语言里(特别是动态语言),第一份代码要比第二份代码要高效得多。
//1
int len = array.length;
for(int i = 0; i < len; ++i){
}
//2
for(int i = 0; i < array.length; ++i){
}
总结:
回到最初的问题:多线程共享非volatile变量,会不会可能导致线程while死循环?
事实上这事要看非常多别的东西的脸色。。编绎器的,CPU的,语言规范的。。
对于没有被编绎器优化掉的代码,CPU的Cache一致性协议(典型MESI)保证了,不会出现死循环的情况。这个不是volatile的功劳,这个仅仅是CPU内部的正常机制而已。
对于多线程同步程序,要小心地在合适的地方加上内存屏障(memory barrier)。
參考:
http://en.wikipedia.org/wiki/Volatile_variable
http://en.wikipedia.org/wiki/MESI
http://en.wikipedia.org/wiki/Write-back#WRITE-BACK
http://en.wikipedia.org/wiki/Bus_snooping
http://en.wikipedia.org/wiki/CPU_cache#Multi-level_caches
http://blog.jobbole.com/36263/ 每一个程序猿都应该了解的 CPU 快速缓存
http://stackoverflow.com/questions/4232660/which-is-a-better-write-barrier-on-x86-lockaddl-or-xchgl
http://stackoverflow.com/questions/8891067/what-does-the-lock-instruction-mean-in-x86-assembly
http://gcc.gnu.org/onlinedocs/gcc-4.6.2/gcc/Atomic-Builtins.html
http://en.cppreference.com/w/cpp/atomic
并行编程之多线程共享非volatile变量,会不会可能导致线程while死循环的更多相关文章
- 多线程同步工具——volatile变量
关于volatile,找了一堆资料看,看完后想找一个方法去做测试,测了很久,感觉跟没有一样. 这本书<深入理解Java内存模型>,对volatile描述中有这样一个比喻的说法,如下代码所示 ...
- Java多线程 -- 正确使用Volatile变量
Java 语言中的 volatile 变量可以被看作是一种 “程度较轻的 synchronized”:与 synchronized 块相比,volatile 变量所需的编码较少,并且运行时开销也较少, ...
- python编程系列---多线程共享全局变量出现了安全问题的解决方法
多线程共享全局变量出现了安全问题的解决方法 当多线程共享全局变量时,可能出现安全问题,解决机制----互斥锁:即在在一段与全局变量修改相关的代码中,假设一个时间片不足以完成全局变量的修改,就在这段代码 ...
- 【Java并发编程】:加锁和volatile变量
加锁和volatile变量两者之间的区别: 1.volatile变量是一种稍弱的同步机制在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比syn ...
- 7.Python网络编程_多线程共享全局变量问题
Python多线程支持全局变量的共享操作,但是它存在很多问题,先来看以下程序,该程序理论上执行完毕后全局变量g_num的值应该是2000000,但是在实际运行中,结果不足理论值 import thre ...
- Java并发编程、内存模型与Volatile
http://www.importnew.com/24082.html volatile关键字 http://www.importnew.com/16142.html ConcurrentHash ...
- [Java并发编程(三)] Java volatile 关键字介绍
[Java并发编程(三)] Java volatile 关键字介绍 摘要 Java volatile 关键字是用来标记 Java 变量,并表示变量 "存储于主内存中" .更准确的说 ...
- JAVA并发编程:相关概念及VOLATILE关键字解析
一.内存模型的相关概念 由于计算机在执行程序时都是在CPU中运行,临时数据存在主存即物理内存,数据的读取和写入都要和内存交互,CPU的运行速度远远快于内存,会大大降低程序执行的速度,于是就有了高速缓存 ...
- Python3 系列之 并行编程
进程和线程 进程是程序运行的实例.一个进程里面可以包含多个线程,因此同一进程下的多个线程之间可以共享线程内的所有资源,它是操作系统动态运行的基本单元:每一个线程是进程下的一个实例,可以动态调度和独立运 ...
随机推荐
- bzoj列表2
之前发过一次了,这里的题较水,没什么好讲的 bzoj1088 直接穷举前两位即可,话说程序员的扫雷是白玩的? bzoj1083 裸的最小生成树(最小生成树=最小瓶颈树),SCOI大丈夫(话说网上二分是 ...
- 【原创】batch-GD, SGD, Mini-batch-GD, Stochastic GD, Online-GD -- 大数据背景下的梯度训练算法
机器学习中梯度下降(Gradient Descent, GD)算法只需要计算损失函数的一阶导数,计算代价小,非常适合训练数据非常大的应用. 梯度下降法的物理意义很好理解,就是沿着当前点的梯度方向进行线 ...
- eclipse 项目报错问题
所有的问题在windoes-->show view--->Problems里查看
- 问题:贴友关于CSS效果的实现
今日在百度贴吧中,一贴有提出如下问题: 对于这个问题,咱们贴上代码看效果 1: <html> 2: <head> 3: <meta http-equiv="co ...
- uva 11468 Substring
题意:给你 k 个模板串,然后给你一些字符的出现概率,然后给你一个长度 l ,问你这些字符组成的长度为 l 的字符串不包含任何一个模板串的概率. 思路:AC自动机+概论DP 首先用K个模板构造好AC自 ...
- Linode各机房在中国访问速度性能测试
最近因为google的各种被X的原因,想自己弄个VPS玩玩,比来比去都推荐linode. 因为各种性能测试工具都不靠谱,还是自己机器来的直接,虽然笨拙但是真实可信. 从测试结果上看,明显东京机房的速度 ...
- POJ2778&HDU2243&POJ1625(AC自动机+矩阵/DP)
POJ2778 题意:只有四种字符的字符串(A, C, T and G),有M中字符串不能出现,为长度为n的字符串可以有多少种. 题解:在字符串上有L中状态,所以就有L*A(字符个数)中状态转移.这里 ...
- Web缓存基础:术语、HTTP报头和缓存策略
简介 对于您的站点的访问者来说,智能化的内容缓存是提高用户体验最有效的方式之一.缓存,或者对之前的请求的临时存储,是HTTP协议实现中最核心的内容分发策略之一.分发路径中的组件均可以缓存内容来加速后续 ...
- WinForm中当TextBox重新获得焦点时输入法失效问题
在winform 中,每当TextBox获得焦点时,部分输入法会失效(如智能ABC.五笔98.极品五笔等),需要重新切换输入法才能正常使用. 此时要将Form的ImeMode属性改为:OnHalf(或 ...
- Keil uCos 2.52 stm32 【worldsing笔记】
1.uCOSii V2.52 a.加了7个可以配置的钩子函数宏 #define OS_TASK_CREATE_HOOK_EN 0 /* 任务创建时调用钩子函数 使能 ...