1.什么是互斥
在计算机执行过程中,对于多个任务,它们共享着一个资源,要求对该资源的存取过程是排他的。
2.为什么要有互斥
不考虑SMP情况,仅分析单CPU情况,因为SMP只不过是更复杂的一种情况,原理类似。
有如下代码片段,其中share_data是一个全局变量。
int share_data = 0;
void foo(void)
{
share_data++; //写共享资源
show share_data; //读共享资源
}
2.1线程间
如果两个线程都执行上述片段,执行过程如下图所示:
首先是Thread A开始执行,此时share_data为0,执行完①后,如果执行流程不被打断,则继续往下走,最终show share_data得到的数据应该是1。但是当Thread A执行完步骤①后,CPU发生了调度,切到Thread B上执行②,而share_data对于两个线程是共享的,所以Thread B此刻看到的share_data是经过了一次自增的,为1。最终执行完流程③,④。Thread A看到的结果是share_data自增了两次,与预期不符。
2.2线程与中断间
还一种情况是代码片段分别在一个线程和中断中:
过程和上面类似,只不过执行流程是被中断打断的。
3.并发和竞态
以上多个执行流在一个时间段并发(Concurrency)执行,并且围绕共享资源进行访问导致了竞态(Race Conditions)。就是一种你争我夺的状态。而这个共享的资源就是他们争夺的对象,可以是(全局的变量,同一个硬件资源,文件系统上的一个文件等)。
与并发常常一起提到的另一个词叫并行,并行指同一个时刻,同时进行,例如SMP上多个CPU执行多个线程这种情况。
3.1如何解决竞态
以上看到了竞态带来了我们预期之外的效果,按照我们程序的设计,对于一个功能模块,同样的输入应该得到一致的输出才行。考虑生活中一个这样的问题:A和B住一起,冰箱里面没面包了就要去买,但是我们要避免两个人重复买面包这种情况。如何解决?
- 方案一:
打开冰箱,发现没面包了,留张纸条贴在冰箱上,买回面包放进去,撕去纸条。用代码表示:
do
{
if (noNote)
{
if (noBread)
{
leave Note;
buy bread;
remove Note;
}
}
}while (1);
能解决问题吗?不能。假设A看到 if (noNote) ---> if (noBread)
,刚好有别的事打断了他,A出去了,这时B进来,也看到了if (noNote) ---> if (noBread)
,然后恰巧B也被打断了,A此时进来,于是他继续前面被打断的工作,留下纸条,买回面包,撕去纸条,走了。然后B回来了,继续前面被打断的工作,留下纸条,买回面包,撕去纸条,走了。这时会发现A和B都买了面包。
- 方案二:
以上问题看上去好像是没提前贴好纸条导致,那如果先贴纸条呢
do{
leave Note;
if (noNote)
{
if (noBread)
{
buy bread;
remove Note;
}
}
}while(1);
分析这个过程看得到,双方留下纸条后,进去检查if (noNote)
都是不成立的,这样,两个人都不会去买面包,如果这种巧合一直按照这种顺序发生,那么永远不会有人买面包。
- 方案三:
标签加以区别:
do{
leave my Note;
if (no Other's Note)
{
if (noBread)
{
buy Bread;
}
remove my Note;
}
}while(1);
显而易见,依旧是不行的。
- 方案四:
do{
leave Note1;
while (is Note2 Exist)
{
do nothing;
}
if (noBread)
{
buy Bread;
}
remove Note1;
}while(1);
do{
leave Note2;
if (noNote1)
{
if(noBread)
{
buy Bread;
}
}
remove Note2;
}while(1);
这个方案可行吗?可行。但是明显看的出和前面3个方案的不同,这里用了两段不同的代码。此方案有如下缺点:
- 同样是2个人买面包,上面三种方案,购买流程一套代码即可实现,而此情况需要2套代码。
- 如果处理线程数超过2个,处理逻辑的复杂度会呈指数级增长。
- 并且第一段处理有一个死循环,如果下面一段的处理比较耗时,则上一段的死循环会持续很久,导致比较高的CPU占用。
- 方案五:
既然方案四已经否定了,综合前三个解决方法来看,可以看出,主要是因为放下纸条和检查纸条这个过程被打断了,2个动作可以拆分为2次完成。如果通过一些手段把这两个动作绑定在一起,看看会是怎么样。
do
{
while(check_and_leave()); //执行不可打断
if (noBread)
{
buy Bread;
}
remove Note;
}while(1);
check_and_leave()里面的逻辑是:
if (noNote)
{
leave Note;
return FALSE;
}
else
{
return TRUE;
}
这样就可以解决竞态问题了。只要实现check_and_leave()执行动作的不可打断就可以了。
4.临界区
4.1概念
enter section;
critical section;
exit section;
remainder section;
临界区(critical section):
- 任务执行时,需要互斥的一段区域。
进入临界区(enter section):
- 进入临界区需要先检查是否有人已进入临界区。
- 如果可进入临界区,设置进入标志。
退出临界区(exit section):
- 清除进入临界区标志。
4.2访问规则
- 空闲则进入
没有任务进入了临界区,任何任务都可以进入。 - 繁忙则等待
有任务进入临界区,其他任务都得等待。 - 有限等待
未进入临界区的任务不能无限等待。 - 让权等待(可选)
未进入临界区的任务,应释放CPU使用权(如切换到阻塞态)。
4.3如何实现临界区的访问
4.3.1禁止中断
禁止中断相当于是硬件方法实现,在第三节的分析可知,临界区出现了竞态主要是因为任务调度引起,或者中断引起。而任务调度也是由中断方法实现(如时间片,超时则引发调度,由时钟中断导致),所以禁止了中断,可以实现临界区的访问。
local_irq_save(unsigned int flags);
critical section;
local_irq_restore(unsigned int flags);
进入临界区(enter section):
- 禁止所有中断,保存CPSR。
退出临界区(exit section):
- 使能中断,恢复CPSR。
缺点:
- 禁止中断后,执行的任务将无法响应任何外部的输入,例如信号,中断,一直执行下去。
- 如果临界区执行耗时较长,那么对系统的性能有很大的影响。