先给自己打个广告,本人的微信公众号:嵌入式Linux江湖,主要关注嵌入式软件开发,股票基金定投,足球等等,希望大家多多关注,有问题可以直接留言给我,一定尽心尽力回答大家的问题。
既然是探讨线程同步的相关知识,我们将这简单的四个字拆分开看,实际上就是两个词“线程”和“同步”,所谓线程我相信只要是非裸机操作的软件开发人员都应当有的概念,如果连“线程”的概念都没有,就需要好好补一下操作系统这一门课程知识了。
然后,我们在看“同步”,这里的“同”不是指同时、一起动作,而是协同、协助、互相配合的意思。如进程、线程同步,可理解为进程或线程A和B一块配合,A执行到一定程度时要依靠B的某个结果,于是停下来,示意B运行;B依言执行,再将结果给A;A再继续操作。
在多线程编程里面,一些敏感数据不允许被多个线程同时访问,此时就使用同步访问技术,保证数据在任何时刻,最多有一个线程访问,以保证数据的完整性。
有了上面线程同步的概念的理论知识介绍,我们再来看一个实际的示例,从而引出线程同步的必要性。请看如下代码
#include
#include
#include
#include
#include
#define MAX_CNT 10000
int cnt = 0;
void *pthfun(void *arg)
{
int i = 0;
for (i = 0; i < MAX_CNT; i++) {
cnt++;
}
return NULL;
}
int main(int argc, char *argv[])
{
pthread_t pth_id1, pth_id2;
pthread_create(&pth_id1, NULL, pthfun, NULL);
pthread_create(&pth_id2, NULL, pthfun, NULL);
pthread_join(pth_id1, NULL);
pthread_join(pth_id2, NULL);
if (cnt == 2 * MAX_CNT) {
printf("It is the result as we think, cnt = %d!\n", cnt);
} else {
printf("Error, it is not the result as we think, cnt = %d\n!", cnt);
}
return 0
}
程序的思想很简单,在main函数中创建了两个线程,这两个线程实现完全相同,分别对全局变量cnt执行++操作,显然按照“常规”理解,线程1对cnt加了MAX_CNT次,线程2对cnt加了MAX_CNT次,执行完线程1和线程2之后,cnt的值应该等于2 x MAX_CNT。
但是结果真的是这样吗?
编译运行上面的程序,打印结果如下,我们发现有的时候cnt的结果跟我们的“常规”理解一样,就是20000(2 x MAX_CNT),但有的时候,这个值比2 x MAX_CNT要小,这是为什么呢?难道我们的打印有问题吗?看起来如此简单的一个自加操作结果,和我们“常规”理解的不一样,是不是很神奇。
查看汇编文件如下:
book@www.100ask.org:/work/linux_knowledge/thread_communi/no_sync_thread$ cat no_sync_thread.s
.file "no_sync_thread.c"
.globl cnt
.bss
.align 4
.type cnt, @object
.size cnt, 4
cnt:
.zero 4
.text
.globl pthfun
.type pthfun, @function
pthfun:
.LFB2:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
movq %rdi, -24(%rbp)
movl $0, -4(%rbp)
movl $0, -4(%rbp)
jmp .L2
.L3:
movl cnt(%rip), %eax
addl $1, %eax
movl %eax, cnt(%rip)
addl $1, -4(%rbp)
.L2:
cmpl $9999, -4(%rbp)
jle .L3
movl $0, %eax
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE2:
.size pthfun, .-pthfun
.section .rodata
.align 8
.LC0:
.string "It is the result as we think, cnt = %d!\n"
.align 8
.LC1:
.string "Error, it is not the result as we think, cnt = %d\n!"
.text
.globl main
.type main, @function
main:
.LFB3:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $48, %rsp
movl %edi, -36(%rbp)
movq %rsi, -48(%rbp)
movq %fs:40, %rax
movq %rax, -8(%rbp)
xorl %eax, %eax
leaq -24(%rbp), %rax
movl $0, %ecx
movl $pthfun, %edx
movl $0, %esi
movq %rax, %rdi
call pthread_create
leaq -16(%rbp), %rax
movl $0, %ecx
movl $pthfun, %edx
movl $0, %esi
movq %rax, %rdi
call pthread_create
movq -24(%rbp), %rax
movl $0, %esi
movq %rax, %rdi
call pthread_join
movq -16(%rbp), %rax
movl $0, %esi
movq %rax, %rdi
call pthread_join
movl cnt(%rip), %eax
cmpl $20000, %eax
jne .L6
movl cnt(%rip), %eax
movl %eax, %esi
movl $.LC0, %edi
movl $0, %eax
call printf
jmp .L7
.L6:
movl cnt(%rip), %eax
movl %eax, %esi
movl $.LC1, %edi
movl $0, %eax
call printf
.L7:
movl $0, %eax
movq -8(%rbp), %rdx
xorq %fs:40, %rdx
je .L9
call __stack_chk_fail
.L9:
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE3:
.size main, .-main
.ident "GCC: (Ubuntu 5.4.0-6ubuntu1~16.04.4) 5.4.0 20160609"
.section .note.GNU-stack,"",@progbits
我们重点看pthfun函数对应的汇编部分,如下:
显然从.L3部分可以看到,在c语言中一句简单的cnt++操作被汇编成了3条语句,分别是:
movl cnt(%rip), %eax
addl $1, %eax
movl %eax, cnt(%rip)
以上三条语句操作分别是:
1. 从ram中读取变量cnt的值,将其存放到寄存器eax中;
2. 将寄存器eax中的值加1
3. 将寄存器eax中的值,回写到ram变量中
因此实际上,计算机在执行操作“cnt++”时,需要执行以上3步操作。而因为我们在main函数中创建了两个线程,执行同样的操作“cnt++”;因为两个线程调度是随机的,有一种可能发生的情况如下:
1. 线程A和线程B分别将变量cnt中的值(假设此时cnt=15000),读取到eax中
2. 线程A将寄存器eax值(15000)加1,此时eax中的值=15000+1=15001
3. 线程B将寄存器eax值(15000)加1,此时eax中的值=15000+1=15001
4. 线程A将eax中的值,回写到cnt对应的内存ram中,此时内存中的值=15001
5. 线程B将eax中的值,也回写到cnt对应的内存ram中,此时内存中的值=15001
6. 变量cnt对应的内存中的值=15001,我们发现实际上只增加了1(并不是两个线程分别加1,最后的结果加了2)
分析到这里,相信此时读者们应该知道了为什么会出现最开始的打印了吧,最后的运算结果跟我们人脑理解的结果不完全一致,这是因为计算机这颗大脑的运行方式并不完全等价于人类大脑。
另外,经过我们上面步骤的分析,留一个小问题给大家思考:同样是上面的程序,cnt变量的计算结果范围是什么?
既然上面的程序在计算机上的运行结果跟我们人类大脑理解的,或是我们期望的值不一致,我相信上面程序的一个目的是两个线程都希望对全局变量cnt加1,我们该如何实现这个目的呢?这就是我们要引出的线程间同步问题。