为什么要同步

8.1、为什么要同步

线程之间是合作关系。既然是合作,那就要由某种约定的规则,否则合作就会出现问题。

例子1

有两个线程同时运行,第一个线程在执行了一些操作后想检查当前的错误状态errno,但在其做检查之前,线程2却修改了errno。这样,当第一个线程再次获得控制权后,检查结果将是线程2改写过的errno,而这是不正确的。

线程1 线程2
。。。。。 。。。。。
读errno变量 。。。。。
。。。。。 写errno变量
从读操作返回 。。。。
检查errno值 。。。。


一个进程的两个线程因为操作不同步而造成线程1运行错误。

之所以出现上述问题,是基于两个原因:

l  errno是线程之间共享的全局变量

l  线程之间的相对执行顺序是不确定的

因此,要解决上述问题有两个办法,即分别消除上述两个原因。消除第1个原因的办法就是限制全局变量,给每个线程一个私有的errno变量。事实上,如果可以将所有的资源都私有化,让线程之间不共享,那么这种问题就不复存在。

但问题是,如果所有资源都不共享,那么还有必要发明线程吗?甚至也没有必要发明进程了。因为这样就违背了进程和线程设计的初衷:共享资源、提高资源利用率。因此,这种解决办法是不切实际的。

那剩下的办法就是消除第2个原因,即让线程之间的相对执行顺序在需要的时候可以确定。

例子2

有两个线程A和B,分别执行指令x=1和x=2,即

线程A:x=1;

线程B:x=2;

请问在上述两个线程结束后,x的值是什么?

由于x是共享变量,且线程之间的相对执行顺序是不确定的,因此线程A先执行,也可以是B先执行。

线程A先执行,那么,x=2;

线程B先执行,那么,x=1。

还有别的结果吗?x有可能等于3吗?

一般情况下,在任何计算机体系结构中x=1对应的不是一条微指令,即一条高级指令对应的是多条微指令,因此一条高级指令需要多个时钟周期(通常为6个时钟周期以上) 才能完成。

例如,x=1的赋值语句就是由多个步骤构成。这些步骤可能包括:先把总线清零,然后把1加到上面去。这样的话,线程A和线程B的执行可能形成如下穿插:线程A把总线清零,线程B把总线清零,线程A将数值1加到总线上,线程B将2加到总线上。这样就由可能出现结果3。

当然了,出现x=3的结果依赖于特定的指令集结构。如果指令集结构在执行赋值语句时不是先将总线清零,然后将要赋值的常数加到总线上,就不会出现结果3,而只能有结果1或者2。

就算赋值语句可以完整(不分割)的执行,编译器的优化也有可能造成不可预料的结果。例如,设定x、y两个变量的初始值为0,进程中的两个线程执行的指令如下:

线程A:r2=x;y=1;

线程B:r1=y;x=2;

该程序运行的结果时什么呢?

r1=0,r2=0是第1种结果;

r1=0,r2=2是第2种结果;

r1=1,r2=0是第3种结果;

r1=1,r1=2是第4种结果。那么,第4种结果会出现吗?

看上去似乎不可能,因为线程A和线程B的指令无论如何穿插,此种结果也不可能出现。遗憾的是,这种推论没有考虑到编译器优化的影响。

由于编译器在进行优化时可以(其实是通常)改变语句的顺序,有可能导致两个线程执行的实际指令顺序如下:

线程A:y=1,r2=x

线程B:x=2,r1=y

而此时,如果两个线程的指令执行进行穿插的话,结果就有可能是r1=1,r2=2了。

 

例子3

线程A

线程B

i=0;

while(i<10)

{

  i++

}

printf “A finished”

i=0

while(i>-10)

{

  i--

printf “B finished”

请问,哪个线程会赢呢?即哪个线程会先运行结束?或者说,有没有哪个线程肯定会赢?答案是不确定的。

如果两个线程恰好是你一步、我一步的执行的话,则两个线程都将无法结束。事实上,如果不采取特殊措施,就没有办法确保谁会赢,也没办法确保是否会结束。

由上述例子可见,引入线程后,也引入了一个巨大的问题,即多线程程序的执行结果有可能是不确定的。而不确定则是我们人类非常反感的东西。那么,如何在保持线程这个概念的同时消除其执行结果的不确定性呢?答案是线程的同步。

posted @ 2020-03-31 07:52  揽月2020  阅读(425)  评论(0编辑  收藏  举报