共计 3646 个字符,预计需要花费 10 分钟才能阅读完成。
引言
本文为第四篇,进程管理之进程同步,本文主要介绍为什么需要进程间同步以及进程间同步的原则和线程同步
一、为什么需要进程间同步
通过两个例子了解一下什么是 进程同步 以及为什么需要进程同步
(1)生产者 - 消费者 问题
问题描述:有一群 生产者进程 在生产产品,并将这些产品提供给 消费者进程 进行消费,生产者进程和消费者进程可以 并发执行 ,在两者之间设置了一个具有 n 个缓冲区 的缓冲池,生产者进程需要将所生产的产品放到一个缓冲区中,消费者进程可以从缓冲区中取走产品消费
生产和消费的过程
当生产者生产了一个产品之后,缓冲区里的产品就会 +1,同样,如果消费者从缓冲区里边消费一个产品,缓冲区里的产品就会 -1,在生活中这种模型是没有问题的(比如生产手机的工厂,流水线上生产完一个手机,就会放在仓库里边,消费者从仓库中取出手机消费,这个生产者 - 消费者模型从宏观的角度上看没有问题)
上边的模型在宏观的角度上看没有问题,但是在计算机微观的角度去看就会有问题。
在计算机中,这个缓冲区是位于 高速缓存或主存 上边的,如果说生产者或消费者要操作里边的数据时,就分为三个步骤:
a、取出数据放到寄存器中 register=count
b、在 CPU 的寄存器中将register+1、register=register+1 表示说生产者完成了一个产品
c、将 register 放回缓冲区 count=register
这三步就是就是我们操作缓冲区必须的三个步骤。我们就可以将缓冲区看作是仓库,将 register 寄存器看作是生产者的地方或者消费者的地方,这个模型我们乍一看,好像也没什么问题
单从生产者程序或者消费者程序去看是没问题的,但是如果两者 并发 的去执行的时候就有可能出现差错
下边红色部分为生产者生产的过程,蓝色的为消费者消费的过程
将 register 和 count 看作是两个部分的值 (假设为 10),假设此时执行生产者的第一步,也就是register=count,此时两者均为10,接着执行生产者的第二步,register=register+1,此时寄存器中的值 + 1 了,那么此时register=11,count=10,假设生产者程序和消费者程序是 并发的执行 的,那么第三步就有可能轮到 消费者 去执行了,那么假设此时到了消费者的第一步 register=count,那么这个时候消费者的进程里边的寄存器的值就是10,接着执行第四步,假设第四步执行到消费者的第二步,也就是register=register-1,此时消费者的进程的寄存器的值就变成9 了,而缓存里边的值还是 10,接着执行第五步,第五步假设执行到消费者的第三个步骤count=register,也就是把寄存器里边的值写回到缓冲区里边,此时缓冲区和消费者的寄存器的值都是 9 了,这里就完成了消费者的操作,接下来还有一个生产者的操作,将生产者的 register 写回到缓冲区里边,那么在刚才,生产者的 register 是等于11,那么执行完这一步,它会将 register 重新的写回到缓冲区中,那么缓冲区中得值就变成了 11,count=register。那这个样子其实就有问题了,刚开始缓冲区的值是 10,而在执行的过程中进行了 + 1 和 - 1 的操作,那么它的值应该还是 10 才对,但是租后却变成了 11,说明这个数据是错误的,错误的原因就在于这两个进程在 并发的执行 了,他们轮流的在操作缓冲区,导致缓冲区中的数据不一致,这个就是生产者 - 消费者的问题
下边看一个实际执行的例子,下边是一个简单的程序,使用了两个线程模拟生产者和消费者的过程:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <vector>
pthread_ mutex t mutex = PTHREAD MUTEX INITIALIZER;
int num = 0;// 全局变量,初始值为 0
void *producer(void*){// 生产者
int times = 10000000 ;
while(times --){// 生产者将 num 循环 + 1 很多次,表示生产过程,消费者是循环 -1
//pthread mutex_ lock (&mutex) ;
num+=1;
//pthread mutex unlock (&mutex) ;
}
}
void *comsumer (void*){// 消费者
int times = 10000000 ;while(times --){//pthread mutex lock (&mutex) ;
num -= 1;
//pthread mutex unlock (&mutex) ;
}
}
// 在 main 函数中创建了两个线程来模拟两个进程,一个线程执行 producer 的逻辑,一个线程执行 comsumer 的逻辑
int main()
{printf("Start a main function.");
pthread_t thread1,thread1;
pthread_create(&thread1, NULL, &producer, NULL);
pthread_create(&thread1, NULL, &comsumer, NULL);
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
printf("Print in main function: num = %d\n", num);
retuen 0
}
因为生产者和消费者循环的次数都是一样的,那么执行的结果应该是 0 才对,那实际的执行结果是不是呢?我们会发现不是,那么这个就是生产者和消费者的问题。上边例子中的缓冲区和 num 就是 临界资源
(2)哲学家进餐问题
问题描述:有五个哲学家,他们的生活方式是交替的进行思考和进餐,哲学家们共同使用一张圆桌,分别坐在周围的五张椅子上,在圆桌上有五个碗五支筷子。平时哲学家们只进行思考,饥饿时则试图取靠近他们左、右两支筷子,只有两支筷子都被他拿到的时候才能进餐,进餐完毕后,放下筷子继续思考
那么这个过程为什么也需要进程同步呢?可以想象一下哲学家进餐的时候会出现什么样的情况,假设在这个时候某一位哲学家饿了,他需要拿起左边的筷子和右边的筷子进行吃饭。这个时候,第一步,拿起左边的筷子,第二步,拿起右边的筷子,假设此时他发现右边的筷子被拿了,那么他就会 等待右边的筷子释放,筷子释放后,他拿起右边的筷子,开始吃饭。这就是哲学家吃饭的时候可能会面临的问题,这样一看,好像没有什么问题。
看一种 极端 的情况,假设这五个哲学家同时肚子饿了,并且同时拿起了 左边 的筷子,然后此时他们就会发现自己 右边 的筷子都被拿了(可以对照上边的圆桌图想象一下),那么此时,五个哲学家都会等待自己右边的筷子被释放,而这个时候所有的筷子都被他们自己拿起来了,所以他们都会相互等待而拿不到筷子,并且他们也都不会释放自己左边的筷子,因此这五个哲学家就会饿死,这个就是最极端的情况
上边就是哲学家进餐问题,现在把筷子换成 资源 ,把哲学家换成 进程 ,这个就是计算机进程所面临的问题,筷子就是 临界资源。
总结一下发生上边两个问题的根源是什么?
- 根源问题是:彼此之间没有进行通信
- 第一个生产者 - 消费者的问题,我们假设生产者通知消费者我已经完成了一件生产
- 第二个哲学家进餐问题,假设哲学家对旁边的哲学家说我要进餐了,这个时候就不会出现问题了
因此得出结论
需要进程间的同步,那么进程间同步是为了解决什么问题呢?
1、对竞争资源在多进程间进行使用次序的协调
2、使得并发执行的多个进程之间可以有效使用资源和相互合作
二、进程间同步的原则
临界资源 :临界资源指的是一些虽作为 共享资源 却又无法同时被多个进程或线程共同访问的共享资源。当有进程使用临街资源时,其它进程必须依据操作系统的 同步机制 等待占用进程释放该共享资源才可重新竞争使用共享资源
为了对临界资源进行有效的约束,就提出了 进程间同步的四个原则
- 空闲让进:资源无占用,允许使用
- 忙则等待:资源被占用,请求进程等待
- 有限等待:保证有限等待时间能够使用资源,避免其它等待的进程僵死
- 让权等待:等待时,进程需让出 CPU,也就是进程由执行状态变为阻塞状态,这也是保证 CPU 可以高效使用的前提
进程间同步的方法:
消息队列、共享存储、信号量。会在后边的文章中详细介绍这些进程间同步的方法
三、线程同步
从之前的文章《进程管理之进程实体》中知道,一个进程可能会有一个或多个线程,并且线程是共享进程资源的。那么现在就有个问题,如果多个线程并发的使用进程资源时,会发生什么?其实也同样会出现上边提到的生产者 - 消费者问题和哲学家进餐问题,因此我们得出结论:进程内多线程也需要同步,因为进程里边的线程会并发的去使用进程中的共享资源
线程同步的方法:
- 互斥量:这个是保证多线程可以互斥访问临界资源的一个锁
- 读写锁:这个是应对多读少写或多写少读这种情况而发明出来的锁
- 自旋锁
- 条件变量
这些方法也会在后边的文章详细介绍
在快速变化的技术中寻找不变,才是一个技术人的核心竞争力。知行合一,理论结合实践