协作进程的同步问题了解吗?

421 阅读3分钟

问题引入

我们知道在的操作系统中的多个进程很可能共享数据,并在不同的处理核上并行运行,很显然我们想要的效果当然是这些活动导致的任何改变都不会相互影响,为了达到这一个效果我们就需要进程之间按一定的方式来同步执行。

临界区问题

假设某个系统中有n个进程,每个进程都有一段代码,称为临界区,进程执行该区代码时可能修改公共变量,该系统的的重要特征是当一个进程在临界区执行时,其他进程不允许在他们的临界区同时执行。

在进入临界区前进程应当请求许可,实现这一请求的代码区段称为进入区,临界区之后可以有退出区

互斥锁

为了解决临界区问题,我们可以采用加锁的方式去保护临界区,这就是说当一个进程进入临界区时会得到锁,在他退出去时会释放锁。

每个锁都有一个布尔值available,他的值表示锁是否可用,如果锁可用那么请求就会成功,如果不可用那么改进程就会被阻塞,直到锁被释放。

这种实现主要的缺点就是进程需要忙等待,当有一个进城在临界区时,其他进程在进入临界区时都需要连续不断的去发送请求,以等待锁变为可用,这种互斥锁也被称为自旋锁,这个过程是会消耗CPU的。

不过自旋锁也是有优点的,因为当进程等待锁时是没有上下文的切换的,因此当使用锁的时间较短时,自旋锁还是非常有用的。

信号量

下面我们思考一下,我们该如何实现同时允许n个进程进入临界区呢?这就要用到我们的信号量,与它是一种更加广义的锁。

一个信号量S是一个整型变量,他除了初始化之外只能通过两个标准原子操作:wait()signal()来访问,俗称P和V操作,实现的伪代码如下:

wait(S){
    while(s<=0){
        //....
        S--;
    }
}
​
signal(S){
    //....
    S++;
}

信号量可以用来控制访问具有多个实例的某种资源,信号量的初始值就是可用资源的个数,当进程需要使用资源的时候需要对该信号量执行wait操作(减少信号量的计数),当进程释放资源时,需要对该信号量执行signal操作(增加信号量的计数),当信号量的值为0时,表示所有资源都在使用中,需要使用资源的进程会被足色,直到计数大于0。

上面讲到的互斥锁会存在忙等待的问题,而我们的信号量也会具有同样的问题,为了解决这个问题,信号量操作采取阻塞进程的方式,也就是说当执行wait操作时发现信号量为不为正,他就必须等待,但是这里的等待不是忙等待而是阻塞自己,并将这个进程加入到等待队列中去,等待下一次执行signal操作时再去队列中唤醒进程,使之由阻塞态变为就绪态。

下面可以看看伪代码:

interface Sign{
    value: number,
    list: process[]
}
​
wait(S: Sign){
    S.value = S.value - 1;
    if(S.value<0){
        S.list.push(process)
        block()//挂起当前进程
    }
}
singal(S: Sign){
    S.value = S.value + 1;
    if(S.value<=0){
        const P = S.list.shift();
        wakeup(P)//唤醒进程
    }
}

操作block挂起调用它的进程,操作wakeup重启阻塞的进程,这两个操作都是由操系统提供的。同时要注意的是这里的信号量可能为负值,此时他的绝对值就代表等待的进程数。

乐观锁和悲观锁

我们知道同步的一种方式就是让临界区互斥,这种方式每次只有一个进程可以进入临界区,但是加入我们编辑一个文档,这意味着必须等待一个人编辑完另一个人才能编辑,但是从实际问题出发,如果多个人编辑的不是文档的同一部分,是可以同时编辑的。因此,让临界区互斥的方法(对临界区上锁),具有强烈的排他性,对修我们使用的改持保守态度,我们称为悲观锁。

和悲观锁相反的时乐观锁,我们开发使用的Git就是一个很好的代表,Git允许大家一起修改,修改完之后将结果保存在本地,然后都可以向远程仓库提交,如果出现冲突那就我们必须先解决冲突才能提交。