Linux线程同步(一)---“初识篇”

一 why

先给自己打个广告,本人的微信公众号:嵌入式Linux江湖,主要关注嵌入式软件开发,股票基金定投,足球等等,希望大家多多关注,有问题可以直接留言给我,一定尽心尽力回答大家的问题。
Linux线程同步(一)---“初识篇”_第1张图片

既然是探讨线程同步的相关知识,我们将这简单的四个字拆分开看,实际上就是两个词“线程”和“同步”,所谓线程我相信只要是非裸机操作的软件开发人员都应当有的概念,如果连“线程”的概念都没有,就需要好好补一下操作系统这一门课程知识了。
然后,我们在看“同步”,这里的“同”不是指同时、一起动作,而是协同、协助、互相配合的意思。如进程、线程同步,可理解为进程或线程A和B一块配合,A执行到一定程度时要依靠B的某个结果,于是停下来,示意B运行;B依言执行,再将结果给A;A再继续操作。
在多线程编程里面,一些敏感数据不允许被多个线程同时访问,此时就使用同步访问技术,保证数据在任何时刻,最多有一个线程访问,以保证数据的完整性。

二 example

有了上面线程同步的概念的理论知识介绍,我们再来看一个实际的示例,从而引出线程同步的必要性。请看如下代码

#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要小,这是为什么呢?难道我们的打印有问题吗?看起来如此简单的一个自加操作结果,和我们“常规”理解的不一样,是不是很神奇。
Linux线程同步(一)---“初识篇”_第2张图片

三 分析

查看汇编文件如下:

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函数对应的汇编部分,如下:
Linux线程同步(一)---“初识篇”_第3张图片
显然从.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,我们该如何实现这个目的呢?这就是我们要引出的线程间同步问题。

你可能感兴趣的:(线程同步,linux,线程同步)