之前我们了解到一些线程的基本知识,线程等待,线程分离啊什么的。现在我们用这些知识简单实现一个火车站抢票的功能。
假设一共有100张票,我们开放4个窗口(线程),让每个窗口进行卖票的功能,每个窗口之间是独立的,他们的任务就是卖完这100张票,每卖一张票,就让总票数-1。
void* ThreadStart(void* arg)
{
(void*)arg;
while(1)
{
if(g_tickets > 0)
{
g_tickets--; //总票数-1
//usleep(100000); //模拟一个窗口的堵塞
printf("i am thread [%p],I'll sell ticket number [%d]\n",\
pthread_self(),g_tickets + 1);
}
else
{
break;//没有票了,直接返回该线程
}
}
return NULL;
}
这样写每个线程的任务,看上去好像是没有什么问题,先看看运行结果
好像真的没有什么问题,但是这是建立在每个线程执行一个任务都是很快的情况下,我们现实中每一个买票的过程所花费的时间都不短,这可以理解成一种阻塞。我们在程序中模拟一下这个阻塞的过程,看看会出现什么结果。
不得了了,我们发现好像有几张票没卖出去,又好像1号票被卖了四次,不同的窗口出售了同样的一张票,结果出现了二义性,这个问题很严重,怎么解决呢?这就得用线程安全的知识了
线程安全
通过上面的代码,我们发现多个线程同时运行的时候,在访问临界资源后,使得程序出现了二义性的结果。
线程安全就是为了解决多个线程在同时运行时,在访问临界资源的同时不能让程序出现二义性的结果。
- 临界资源:在同一时刻,一个资源只能被一个线程(执行流)所访问
- 访问:在临界区中对临界资源进行非原子操作
- 原子操作:原子操作使得当前操作是一步完成的,也就是每一个操作只能有两个结果,一个是完成,一个是未完成
再次了解原子性
- 执行流A先从CPU中获得数据 g_tickets = 100,在获取完数据之后发生了阻塞,开始处理这个数据,此时还没有执行 g_tickets-- 的操作。
- 在执行流A处理第100张票的同时,执行流B开始从CPU中获取数据,此时因为执行流A还没有进行 g_tickets-- 的操作,CPU中 g_tickets = 100,执行流B开始处理这个数据,进行 g_tickets–,然后返回给CPU处理后的结果,g_tickets = 99。
- 之后执行流A在执行 g_tickets-- 之后,返回给CPU的结果是 g_tickets = 99。
经过这样一个模拟阻塞的过程,发现原本应该是 g_tickets = 98 的结果,却因为二义性导致结果是 g_tickets = 99。这就是由于执行流A执行的 g_tickets-- 操作是非原子的操作,也就是执行流A在执行的时候,可能会遇到时间片耗尽,从而导致执行流A被调度。相当于执行流A在执行时的任何一个地方都可能会被打断。
如何保证线程安全
- 互斥:保证在同一时刻只能有一个执行流访问临界资源,如果多个执行流想要同时问临界资源,并且临界资源没有执行流在执行,那么只能允许一个执行流进入该临界区。
也就是如果执行流A,B,C…想要同时访问临界资源,这时就只能有一个执行流先访问临界资源,假设此时访问的是执行流A,其他执行流B,C…不能打断执行流A的执行。 - 同步:保证程序对临界资源的合理访问。也就是执行流A执行完自己的任务时,必须让出CPU资源,不能一直占着CPU,应该及时让出CPU,使其他执行流也可以执行他们自己的任务。
看着图片“形象”的再来理解一次,假设有一个厕所,互斥就是同一时间只能有一个滑稽去上厕所,其他滑稽只能在外面排队;同步就是滑稽A上完厕所后不能占着茅坑不拉* ,应该赶紧出去让出坑位给其他滑稽。
想要做到这几点,本质上就是需要一把锁,也就是互斥量 (mutex)。
互斥锁
互斥锁是用来保证互斥属性的一种操作
互斥锁的底层是互斥量,而互斥量**(mutex)**的本质是一个计数器,这个计数器只有两个状态,即 0 和 1 。
- 0:表示无法获取互斥锁,也就是需要访问的临界资源不可以被访问
- 1:表示可以获取互斥锁,也就是需要访问的临界资源可以被访问
加锁与解锁
加锁的过程可以使用互斥锁提供的接口,以此来获取互斥锁资源
- 互斥量计数器的值为1,表示可以获取互斥锁,获取到互斥锁的接口就正常返回,然后访问临界资源。
- 互斥量计数器的值为0,表示不可以获取互斥锁,当前获取互斥锁的接口进行阻塞等待。
加锁操作:对互斥锁当中的互斥量保存的计数器进行减1操作
解锁操作:对互斥锁当中的互斥量保存的计数器进行加1操作
看到这里,就可以简单的理解为加锁和解锁就是这样的一个过程
那么问题来了,我们在购票代码中改变票数的操作就是 g_tickets–,这个的本质就是减一。互斥量计数器本身也是一个变量,这个变量的取值是0/1,对于这样一个变量进行加一减一操作的时候,这就是原子性操作吗?
这时候不禁想起老爹的那句话
所以说,想要解决原子性的问题,还得要用原子性的操作,怎么一步就判断有没有加锁呢?
汇编中有一个指令xchgb,可以用来交换寄存器和内存中的内容,这个操作就是原子性的,一步到位。
我们再来分析一下,如果是需要加锁的情况,互斥量计数器最后就会从1变成0;如果是不能加锁的情况,互斥量计数器中的值还是0,也就是判断之