上一篇学习了基本的进程和线程,这里继续学习Windows的并发与同步
《Windows内核原理与实现》

并发与同步

为了保证操作系统和应用有序地执行,它们读取和访问的数据应当是有效的。如果A线程使用了资源甲,进行了修改,同时B线程也对资源甲进行了修改,保存时就会产生冲突,所以我们需要一种(多种)机制去控制各个线程的顺序使得它们对资源的使用是协调的、合理的、安全的。

并发的来源

基础

理想情况下,如果所有进程都具有执行条件,系统会直接根据优先级顺序将处理器资源分配给各个线程。因而唯一可能的线程冲突就是优先级的冲突。这很好解决,只要让高优先级的优先执行就好了。但是很有可能有的线程依赖另一个线程,或者依赖系统的某个信号和状态。

看下面这个书中的例子

1
2
3
4
5
6
7
8
extern int g_nCount;
DWORD ComputeThreadProc(PLVOID pParam){
g_nCount = 0;
while(g_nCount++<100){
//
}
return 0;
}
1
2
3
4
5
6
7
extern int g_nCount;

int ControlThreadProc(PLVOID pRaram){
CreateThread(ComputeThreadProc);
g_nCount=100;
return 0;
}

如果执行ControlThreadProc函数,可能发生什么,ComputeThreadProc中的汇编可能把g_nCount保存在两个寄存器中,用RCX来循环,用RAX来执行每一轮最后的判断,那么如果主线程内执行了g_nCount=100;,却被RCX赋值了,那么它就不会停止

image-20241118135849061

所以,多线程通信时,必须保证对变量的操作为原子操作或者对变量的访问是互斥的。

处理器中有些自带的原子操作命令,基本的指令前加lock即是原子操作:

1
2
3
4
5
6
7
8
9
LONG FASTCALL InterlockerIncrement(IN OUT LONG volatile *addr)
{
__asm
{
mov eax,1
mov ecx,addr
lock xadd [ecx],eax
}
}

这样,通过lock xadd,这组指令就会变成原子操作,其它线程的操作不能插入到这个过程中

通讯与同步

正如上面所说,为了使程序以正常的顺序进行,我们需要一种机制去协调进程/线程对某些数据的操作,以通讯为基础的同步机制就这样诞生了。下面的操作均为原子操作

互斥和互斥体

互斥指一个共享资源,任何时刻只能有一个主体可以访问。Windows中可以通过互斥体实现跨进程的访问

互斥体同步对象

该对象有两种状态,有信号和无信号,有信号表示该对象当前无人访问。无信号表示其已被某线程拥有,只有当该线程释放所有权后它才会变为有信号状态。

信号量

对于一个资源,有一个标识值说明当前有多少对象在使用这个资源,这个值是信号量,当值为x时,表示还可以有x个对象访问它,访问叫做DOWN,每次访问使x-1,如果x本身为0,当前线程阻塞。当对象访问结束时,执行UP操作,使x+1然后令一个阻塞的线程执行DOWN获得资源的操作权限

和前面的类似,如果值为1,则不可访问并被阻塞,如果为0则可以访问,同时有局部和全局之分,如果是局部锁,它只控制一个小区域,反之是全局的控制。

临界区

对于一段临界区代码,同一时刻只有一个线程在其中执行。如果一个数据只在临界区中被访问,那么这个数据也是线程安全的。

临界区可以不连续,但是在进入时必须触发enter函数,离开时触发leave函数以用上面几种方法防止同时访问

自旋锁和忙等待

忙等待:不断重复检查以第一时间获得信息。
自旋锁通过忙等待实现一个CPU在一段时间内对一个资源的同时拥有,即其它CPU会在这时重复检查是否可以使用这个资源,直到所有权释放

消息

接收和发送。两个进程可以相互接收和发送消息,具体实现需要依赖其它同步原语

同步问题

死锁

如果一组线程中的每个线程都在等待只能由其他线程才能满足的条件,那 么这组线程是死锁的,所有的线程都将继续等待,无法前进。

银行家算法

饥饿

饥饿是指一个进程或线程满足执行的条件,但一直得不到执行,甚至 永远得不到执行(“饿死”)。饥饿通常是由资源分配策略引起的,比如由于策略而导 致不公平,有些资源请求在特定的情形下永远得不到满足。

优先级反转

优先级反转是指这样的问题:在线程调度时,一个低优先级的线程占有一个共享资源, 从而导致高优先级的线程虽然其他运行条件都满足,但因为得不到该资源而无法运行。在 采用抢占式调度算法的操作系统中,当低优先级的线程占有了资源时,它有可能被中等优 先级的其他线程抢占,从而导致高优先级的线程在更长时间内无法运行,实际的效果就是, 相当于把高优先级的线程降到低优先级了。

中断与异常

中断

其它设备(比如IO)需要CPU响应时,会发送硬件中断给CPU,令其执行那个设备需要执行的内容。内核中的代码也可以发送软件中断,比如执行线程调度。CPU可以屏蔽中断用来专注执行一段指令,也就是使其变为原子操作。

异常

异常也分为软件异常和硬件异常。它和当前的代码/线程有关。硬件异常有:除零错误,缺页错误等。软件异常有:软件断点陷阱等。当触发异常时,会按照固定流程执行处理异常的代码,并导致不同的结果(崩溃/继续执行)