进程管理(三)进程同步
基本概念:
进程同步概念:
进程同步指的是协调多个并发执行的进程或线程之间的操作顺序,以确保它们按照既定的方式交互和共享资源。进程同步的目的是防止竞争条件(Race Condition)、死锁(Deadlock)、活锁(Livelihood)、饥饿(Starvation)等并发编程中常见的问题,确保系统的正确性、可靠性和稳定性
eg:假设计算2+5*6,系统产生了2个进程: 一个加法,一个乘法;为了保证计算正确,则必须需要先做乘法进程,之后再处理加法进程。所以需要一定的机制约束加法进程要后执行
临界区:
多个进程可以共享系统资源,但是有许多资源一次只能够给一个进程所用,将一次仅仅允许一个进程使用的资源称为临界资源。如设备(打印机等),内存,文件,变量等。临界区的目的就是确保任何时刻,只有一个线程或进程可以进入临界区执行(互斥的访问),以避免竞态条件(Race Condition)和数据不一致性等并发问题
临界区访问过程:
- 进入区:为了进入临界区使用临界资源,在进入区要检查可否进入临界区,若能进入临界区,应设置正在访问临界区的标志,以阻止其他进程同时进入临界区
- 临界区:进程中访问临界资源的那段代码
- 退出区:将正在访问临界区的标志清除
- 剩余区:代码中剩余的部分
同步:
也称为制约关系,指为了完成某任务而建立的两个或多个进程,这些进程需要在某些位置上协调它们的工作次序而等待、传递信息所产生的制约关系。
eg:输入进程A通过单缓冲 向进程B提供数据,当该缓冲区空,进程B则不能获取数据而阻塞。 当进程A将数据送入缓冲区时,进程B就被唤醒。反之,当缓冲区满时,进程A被阻塞,仅当进程B取走缓冲区时,才唤醒进程A
互斥:
称为间接制约关系,当一个进程进入临界区使用临界资源时,另外一个进程必须等待,当临界资源的进程退出临界区后,另外一个进程才运行访问该临界资源
信号量
原语: 原语指的是 完成某种功能且不被分割、不被中断执行的操作序列,通常可由硬件来实现。
信号量: 信号量机制是一种用于控制对共享资源的访问的同步机制,常用于多线程或多进程的并发编程中。信号量维护一个整型计数器和一个等待队列,用于实现对临界资源的互斥访问和线程间的协调通信。
信号量的基本操作包括两个原子操作:wait
(等待)(P操作)和 post
(释放)(V操作)
wait
操作(也称为 P 操作):当一个线程或进程希望访问临界资源时,首先执行wait
操作。如果信号量的计数器大于零,则将计数器减一,并允许线程继续执行;否则,线程会被阻塞,直到计数器变为大于零为止post
操作(也称为 V 操作):当一个线程或进程使用完临界资源后,执行post
操作来释放资源。该操作会将信号量的计数器加一,并唤醒等待队列中的一个线程,使其可以继续执行
二元信号量的计数器只能取 0 或 1,用于表示资源的空闲或占用状态。常用于实现互斥锁的功能
计数信号量的计数器可以取任意非负整数,用于表示可用资源的数量。常用于限制并发访问的数量或者表示资源池的状态
**管程:**管程(Monitor)是一种用于同步并发程序的抽象数据类型,提供了一种机制来限制对共享资源的访问,以确保线程或进程间的协作和互斥
管程的组成:
- 共享数据结构(Shared Data Structure): 管程中包含了需要被多个线程或进程共享的数据结构,这些数据结构可能包含了共享资源或者用于线程间通信的缓冲区
- 操作(Operations): 管程中定义了一系列操作或函数,用于对共享数据进行访问和操作。这些操作可能包括获取资源、释放资源、等待条件、唤醒等待的线程等
- 条件变量(Condition Variables): 管程中通常包含了一个或多个条件变量,用于实现线程间的等待和唤醒机制。条件变量可以用来表示一些特定的条件,当条件不满足时,线程可以等待在条件变量上,直到其他线程发出信号唤醒它
- 互斥锁(Mutex): 为了确保对共享资源的互斥访问,管程通常包含了一个互斥锁,用于控制多个线程对共享资源的访问。只有持有互斥锁的线程才能访问共享资源,其他线程需要等待锁的释放
经典同步问题
生产者 - 消费者问题
问题简述:
- 存在一个共享的有限缓冲区,生产者和消费者共享该缓冲区
- 生产者的任务是生产产品,并将其放入缓冲区、
- 消费者的任务是从缓冲区中取出产品并进行消费
- 缓冲区为空时,消费者必须等待,直到有产品可用
- 缓冲区已满时,生产者必须等待,直到有空间可用
- 生产者和消费者之间的操作必须同步,以避免竞争条件和数据不一致性
生产者-消费者问题的关键在于如何实现生产者和消费者之间的协调,以及对共享资源的互斥访问
需要信号量、管程、条件变量等同步机制来确保生产者和消费者的正确操作
buffer: array of length N // 共享缓冲区
in, out: integer // 缓冲区的读写指针
semaphore mutex = 1 // 互斥锁,用于对缓冲区的访问进行互斥
semaphore fullSlots = 0 // 表示已满的槽位数量,初始为0
semaphore emptySlots = N // 表示空闲的槽位数量,初始为缓冲区大小N
procedure producer:
while true:
item = produceItem() // 生产物品
wait(emptySlots) // 等待有空闲槽位可用
wait(mutex) // 获取对缓冲区的互斥访问
buffer[in] = item // 将物品放入缓冲区
in = (in + 1) % N // 更新写指针
signal(mutex) // 释放对缓冲区的互斥访问
signal(fullSlots) // 增加已满的槽位数量
procedure consumer:
while true:
wait(fullSlots) // 等待有产品可消费
wait(mutex) // 获取对缓冲区的互斥访问
item = buffer[out] // 从缓冲区取出物品
out = (out + 1) % N // 更新读指针
signal(mutex) // 释放对缓冲区的互斥访问
signal(emptySlots) // 增加空闲的槽位数量
consumeItem(item) // 消费物品
mutex
信号量用于实现对共享缓冲区的互斥访问。在生产者和消费者操作缓冲区时,必须先获取mutex
信号量,以确保同一时刻只有一个线程可以访问缓冲区,避免数据竞争和不一致性fullSlots
和emptySlots
信号量分别用于表示已满的槽位数量和空闲的槽位数量。生产者在生产物品时,必须等待至少有一个空闲槽位可用,而消费者在消费物品时,必须等待至少有一个已满的槽位可用- 在生产者生产物品和消费者消费物品之后,分别通过
signal(fullSlots)
和signal(emptySlots)
来更新已满和空闲槽位的数量,以通知等待的生产者和消费者可以继续操作
读者 - 写者问题
问题简述:
- 存在一个共享的数据结构(如一个文件、数据库等),可以被多个读者同时访问,但只允许一个写者独占访问
- 读者的任务是从共享资源中读取数据,多个读者可以同时访问共享资源,且读者之间不会相互影响
- 写者的任务是向共享资源中写入数据,写者需要独占地访问共享资源,且不允许同时存在多个写者或读者和写者同时访问
- 当有写者在对共享资源进行写操作时,不允许其他任何读者或写者访问共享资源
- 当有读者在对共享资源进行读操作时,其他读者可以同时访问共享资源,但不允许写者访问共享资源
- 读者和写者之间的操作必须同步,以避免竞态条件和数据不一致性
mutex: semaphore = 1 // 用于对读者数量和写者访问的互斥
writeInProgress: semaphore = 0 // 表示是否有写者正在访问
readers: integer = 0 // 表示当前正在访问共享资源的读者数量
procedure reader:
wait(mutex) // 获取对读者数量的互斥访问
readers = readers + 1 // 增加读者数量
if readers == 1:
wait(writeInProgress) // 如果是第一个读者,则等待写者结束
signal(mutex) // 释放对读者数量的互斥访问
// 读取共享资源
read_shared_data()
wait(mutex) // 获取对读者数量的互斥访问
readers = readers - 1 // 减少读者数量
if readers == 0:
signal(writeInProgress) // 如果没有读者了,唤醒可能等待的写者
signal(mutex) // 释放对读者数量的互斥访问
procedure writer:
wait(mutex) // 获取对写者访问的互斥
if readers > 0 or writeInProgress == 1:
signal(mutex) // 如果有读者或者其他写者正在访问,则等待
wait(writeInProgress) // 等待当前写者结束
writeInProgress = 1 // 表示当前有写者正在访问
signal(mutex) // 释放对写者访问的互斥
// 写入共享资源
write_shared_data()
writeInProgress = 0 // 写者结束访问
signal(writeInProgress) // 唤醒其他等待的写者或读者
哲学家进餐问题
问题的描述:
- 有五位哲学家围坐在一张圆桌周围,每位哲学家面前都有一碗意大利面
- 每位哲学家都需要两把餐叉才能进餐,一把放在左边,一把放在右边
- 哲学家的生活包括两个活动:思考和进餐。当哲学家饿了时,他试图进餐,此时他会先尝试拿起他左右两边的餐叉
- 如果他拿到了两把餐叉,他就会进餐一段时间,然后把餐叉放回原处继续思考
- 如果他无法同时拿到两把餐叉,他就会放下已经拿到的餐叉,继续思考等待
- 问题的关键在于如何确保每位哲学家都能有机会进餐,并且避免死锁(即每位哲学家都拿起了一只餐叉,但无法再拿到另一只导致永远无法进餐)
const int N = 5; // 哲学家数量
enum { THINKING, HUNGRY, EATING } state[N]; // 每位哲学家的状态
semaphore mutex = 1; // 用于对临界区的互斥访问
semaphore S[N]; // 用于表示每位哲学家是否可以进餐的信号量数组
void philosopher(int i) {
while (true) {
think(); // 哲学家思考
pickup_forks(i); // 哲学家拿起叉子
eat(); // 哲学家进餐
putdown_forks(i); // 哲学家放下叉子
}
}
void pickup_forks(int i) {
wait(mutex); // 进入临界区
state[i] = HUNGRY; // 设置哲学家状态为饥饿
test(i); // 尝试拿叉子
signal(mutex); // 离开临界区
wait(S[i]); // 如果拿不到叉子,则等待
}
void putdown_forks(int i) {
wait(mutex); // 进入临界区
state[i] = THINKING; // 设置哲学家状态为思考
test((i + N - 1) % N); // 检查左邻居是否可以进餐
test((i + 1) % N); // 检查右邻居是否可以进餐
signal(mutex); // 离开临界区
}
void test(int i) {
if (state[i] == HUNGRY && // 如果当前哲学家饥饿
state[(i + N - 1) % N] != EATING && // 左邻居不在进餐
state[(i + 1) % N] != EATING) { // 右邻居不在进餐
state[i] = EATING; // 哲学家可以进餐
signal(S[i]); // 唤醒哲学家
}
}