为什么要同步
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” |
请问,哪个线程会赢呢?即哪个线程会先运行结束?或者说,有没有哪个线程肯定会赢?答案是不确定的。
如果两个线程恰好是你一步、我一步的执行的话,则两个线程都将无法结束。事实上,如果不采取特殊措施,就没有办法确保谁会赢,也没办法确保是否会结束。
由上述例子可见,引入线程后,也引入了一个巨大的问题,即多线程程序的执行结果有可能是不确定的。而不确定则是我们人类非常反感的东西。那么,如何在保持线程这个概念的同时消除其执行结果的不确定性呢?答案是线程的同步。