进程的同步与互斥
目录
一、进程同步
二、进程互斥
1.临界资源访问代码:
①进入区
②临界区
③退出区
④剩余区
注:
2.互斥准则:
①.空闲让进。
②.忙则等待。
③.有限等待。
④.让权等待。
三、进程互斥的软件实现方法
1.单标志法
2.双标志先检查
3.双标志后检查
4.Peterson算法
四、进程互斥的硬件实现方法
1.中断屏蔽方法
2.TestAndSet(TS指令/TSL指令)
3.Swap指令(XCHG指令)
五、软硬件方法是否满足互斥准则的分析:
六、互斥锁
七、信号量
1.整型信号量
2.记录型信号量
3.信号量实现互斥
4.信号量实现同步
5.信号量实现前驱
八、管程(monitor)
1.管程的组成
2.条件变量
一、进程同步
进程具有异步性的特征。异步性是指,各并发执行的进程以各自独立的、不可预知的速度向前推进。
Exp:
读进程和写进程并发地运行,由于并发必然导致异步性,因此“写数据”和“读数据”两个操作执行的先后顺序是不确定的。而实际应用中,又必须按照“写数据→读数据”的顺序来执行的。如何解决这种异步问题,就是“进程同步”所讨论的内容。
同步亦称直接制约关系,它是指为完成某种任务而建立的两个或多个进程,这些进程因为需要在某些位置上协调它们的工作次序而产生的制约关系。进程间的直接制约关系就是源于它们之间的相互合作。
简单来说就是:并发性带来了异步性,有时需要通过进程同步解决这种异步问题。
有的进程之间需要相互配合地完成工作,各进程的工作推进需要遵循一定的先后顺序。
二、进程互斥
进程的“并发”需要“共享”的支持。各个并发执行的进程不可避免的需要共享一些系统资源(比如内存,又比如打印机、摄像头这样的I/0设备)
两种资源共享方式:
互斥共享方式 | 系统中的某些资源,虽然可以提供给多个进程使用,但一个时间段内只允许一个进程访问该资源 |
同时共享方式 | 系统中的某些资源,允许一个时间段内由多个进程“同时”对它们进行访问 |
我们把一个时间段内只允许一个进程使用的资源称为临界资源。许多物理设备(比如摄像头、打印机)都属于临界资源。此外还有许多变量、数据、内存缓冲区等都属于临界资源。
对临界资源的访问,必须互斥地进行。互斥,亦称间接制约关系。进程互斥指当一个进程访问某临界资源时,另一个想要访问该临界资源的进程必须等待。当前访问临界资源的进程访问结束,释放该资源之后另一个进程才能去访问临界资源。
1.临界资源访问代码:
对临界资源的互斥访问,可以在逻辑上分为如下四个部分:
①进入区
负责检查是否可进入临界区,若可进入,则应设置正在访问临界资源的标志(可理解为“上锁”),以阻止其他进程同时进入临界区
②临界区
访问临界资源的那段代码
③退出区
负责解除正在访问临界资源的
标志(可理解为“解锁”)
④剩余区
做其他处理
注:
临界区是进程中访问临界资源的代码段。
进入区和退出区是负责实现互斥的代码段。
临界区也可称为“临界段”。
2.互斥准则:
为了实现对临界资源的互斥访问,同时保证系统整体性能,需要遵循以下原则:
①.空闲让进。
临界区空闲时,可以允许一个请求进入临界区的进程立即进入临界区:
②.忙则等待。
当已有进程进入临界区时,其他试图进入临界区的进程必须等待;
③.有限等待。
对请求访问的进程,应保证能在有限时间内进入临界区(保证不会饥饿);
④.让权等待。
当进程不能进入临界区时,应立即释放处理机,防止进程忙等待。
三、进程互斥的软件实现方法
1.单标志法
算法思想:两个进程在访问完临界区后会把使用临界区的权限转交给另一个进程。也就是说每个进程进入临界区的权限只能被另一个进程赋子。
在单标志法中,进程间通过一个共享变量turn来控制谁可以进入临界区。
#include <stdio.h>#include <pthread.h>int turn = 0; // 标志,0表示P0进程,1表示P1进程void *process_P0(void *arg) {while (1) {while (turn != 0); // 等待 P1 交出权限// 进入临界区printf("P0 is in the critical section.\n");// 离开临界区turn = 1; // 交出权限给P1}}void *process_P1(void *arg) {while (1) {while (turn != 1); // 等待 P0 交出权限// 进入临界区printf("P1 is in the critical section.\n");// 离开临界区turn = 0; // 交出权限给P0}}int main() {pthread_t t0, t1;pthread_create(&t0, NULL, process_P0, NULL);pthread_create(&t1, NULL, process_P1, NULL);pthread_join(t0, NULL);pthread_join(t1, NULL);return 0;}
2.双标志先检查
算法思想:设置一个布尔型数组flag0,数组中各个元素用来标记各进程想进入临界区的意愿,比如“flag[0]=ture”意味着0号进程 P0现在想要进入临界区。每个进程在进入临界区之前先检查当前有没有别的进程想进入临界区,如果没有,则把自身对应的标志fag[i]设为true,之后开始访问临界区。
每个进程通过flag数组表示是否想进入临界区,进入之前会检查对方是否有意图进入临界区。
#include <stdio.h>#include <pthread.h>int flag[2] = {0, 0}; // flag[0] 表示 P0 是否想进入,flag[1] 表示 P1void *process_P0(void *arg) {while (1) {flag[0] = 1; // P0 想要进入while (flag[1]); // 如果 P1 也想进入,等待// 进入临界区printf("P0 is in the critical section.\n");// 离开临界区flag[0] = 0; // 离开后,释放标志}}void *process_P1(void *arg) {while (1) {flag[1] = 1; // P1 想要进入while (flag[0]); // 如果 P0 也想进入,等待// 进入临界区printf("P1 is in the critical section.\n");// 离开临界区flag[1] = 0; // 离开后,释放标志}}int main() {pthread_t t0, t1;pthread_create(&t0, NULL, process_P0, NULL);pthread_create(&t1, NULL, process_P1, NULL);pthread_join(t0, NULL);pthread_join(t1, NULL);return 0;}
3.双标志后检查
算法思想:双标志先检查法的改版。前一个算法的问题是先“检査”后“上锁”,但是这两个操作又无法一气呵成,因此导致了两个进程同时进入临界区的问题。因此,人们又想到先“上锁”后“检查”的方法,来避免上述问题。
此方法在设定flag标志后再检查对方的标志,从而减少了竞争条件。
#include <stdio.h>#include <pthread.h>int flag[2] = {0, 0};void *process_P0(void *arg) {while (1) {flag[0] = 1; // 先设置标志while (flag[1]) { // 检查 P1 是否想进入flag[0] = 0; // 暂时放弃// 再次尝试进入flag[0] = 1;}// 进入临界区printf("P0 is in the critical section.\n");// 离开临界区flag[0] = 0;}}void *process_P1(void *arg) {while (1) {flag[1] = 1; // 先设置标志while (flag[0]) { // 检查 P0 是否想进入flag[1] = 0; // 暂时放弃// 再次尝试进入flag[1] = 1;}// 进入临界区printf("P1 is in the critical section.\n");// 离开临界区flag[1] = 0;}}int main() {pthread_t t0, t1;pthread_create(&t0, NULL, process_P0, NULL);pthread_create(&t1, NULL, process_P1, NULL);pthread_join(t0, NULL);pthread_join(t1, NULL);return 0;}
4.Peterson算法
算法思想:结合双标志法、单标志法的思想。如果双方都争着想进入临界区,那可以让进程尝试“孔融让梨”(谦让)。做一个有礼貌的进程。
Peterson 算法通过结合标志位和turn变量,确保进程间互斥访问。
#include <stdio.h>#include <pthread.h>int flag[2] = {0, 0}; // 进程想进入的标志位int turn = 0; // 谁让出临界区的标志void *process_P0(void *arg) {while (1) {flag[0] = 1;turn = 1;while (flag[1] && turn == 1); // 等待 P1 释放临界区// 进入临界区printf("P0 is in the critical section.\n");// 离开临界区flag[0] = 0;}}void *process_P1(void *arg) {while (1) {flag[1] = 1;turn = 0;while (flag[0] && turn == 0); // 等待 P0 释放临界区// 进入临界区printf("P1 is in the critical section.\n");// 离开临界区flag[1] = 0;}}int main() {pthread_t t0, t1;pthread_create(&t0, NULL, process_P0, NULL);pthread_create(&t1, NULL, process_P1, NULL);pthread_join(t0, NULL);pthread_join(t1, NULL);return 0;}
四、进程互斥的硬件实现方法
1.中断屏蔽方法
中断屏蔽通过禁止中断实现进程互斥,但这个方法适用于单处理器系统。
void critical_section() {disable_interrupts(); // 屏蔽中断// 临界区操作enable_interrupts(); // 恢复中断}
2.TestAndSet(TS指令/TSL指令)
TestAndSet 是一种硬件指令,利用它可以实现互斥锁的获取和释放。
int lock = 0;int TestAndSet(int *target) {int old = *target;*target = 1;return old;}void enter_critical_section() {while (TestAndSet(&lock) == 1); // 自旋等待// 临界区操作}void exit_critical_section() {lock = 0; // 释放锁}
3.Swap指令(XCHG指令)
利用 Swap 指令,可以原子性地交换两个变量的值,从而实现互斥。
int lock = 0;void Swap(int *a, int *b) {int temp = *a;*a = *b;*b = temp;}void enter_critical_section() {int key = 1;while (key == 1) {Swap(&lock, &key); // 自旋等待直到成功}// 临界区操作}void exit_critical_section() {lock = 0; // 释放锁}
五、软硬件方法是否满足互斥准则的分析:
在分析这些互斥算法是否满足互斥准则时,可以将四个标准分别解释如下:
- 空闲让进:当没有其他进程在临界区时,一个进程应当能够立即进入临界区。
- 忙则等待:如果另一个进程正在进入或已经在临界区,其他进程必须等待,不能进入临界区。
- 有限等待:每个进程在请求进入临界区后,必须能够在有限的时间内进入临界区,避免无限等待或饥饿问题。
- 让权等待:当一个进程无法进入临界区时,它应该主动放弃 CPU,以便其他进程能够继续执行,而不是无限自旋占用 CPU。
各个算法满足互斥准则的分析表
算法 | 空闲让进 | 忙则等待 | 有限等待 | 让权等待 |
单标志法 | ✅ | ✅ | ❌ | ❌ |
双标志先检查法 | ✅ | ✅ | ❌ | ❌ |
双标志后检查法 | ✅ | ✅ | ❌ | ❌ |
Peterson算法 | ✅ | ✅ | ✅ | ❌ |
中断屏蔽法 | ✅ | ✅ | ✅ | ❌ |
TestAndSet指令 | ✅ | ✅ | ❌ | ❌ |
Swap指令 | ✅ | ✅ | ❌ | ❌ |
详细分析:
- 单标志法:
- 空闲让进:如果对方没有进入临界区,当前进程可以立即进入。
- 忙则等待:如果对方正在使用临界区,当前进程会等待。
- 有限等待:无法保证有限等待,因为进程交替传递访问权,但如果某个进程长时间执行,可能会导致另一个进程无限等待。
- 让权等待:使用的是轮询等待(自旋锁),进程不会主动放弃 CPU。
- 双标志先检查法:
- 空闲让进:没有其他进程想进入时,当前进程可以进入。
- 忙则等待:当前进程会等待其他进程退出临界区。
- 有限等待:没有保证,因为两个进程可能同时设置标志位,造成“忙等”死锁。
- 让权等待:也是自旋锁,没有主动让出 CPU。
- 双标志后检查法:
- 空闲让进:如果没有其他进程在临界区,当前进程可以进入。
- 忙则等待:如果另一个进程设置了标志位,当前进程会等待。
- 有限等待:依然存在竞争条件,无法完全避免无限等待的可能。
- 让权等待:自旋等待,没有让权。
- Peterson算法:
- 空闲让进:如果对方没有设置标志,当前进程可以进入。
- 忙则等待:如果对方已经在临界区,当前进程会等待。
- 有限等待:Peterson 算法保证了进程不会无限等待,能够在有限的时间内进入临界区。
- 让权等待:没有主动让出 CPU,自旋等待。
- 中断屏蔽法:
- 空闲让进:无其他进程时,可以进入临界区。
- 忙则等待:当另一个进程进入临界区时,其他进程无法响应中断,因此无法进入。
- 有限等待:可以保证有限等待,因为进入临界区的进程可以禁用中断,保证临界区的独占性。
- 让权等待:没有自旋,但禁用中断导致其他进程无法运行,也不符合让权等待。
- TestAndSet指令:
- 空闲让进:当没有其他进程锁定时,当前进程可以进入临界区。
- 忙则等待:如果临界区被锁定,当前进程会一直尝试获取锁(自旋)。
- 有限等待:没有保证有限等待,可能会产生饥饿问题。
- 让权等待:是自旋锁,没有主动让出 CPU。
- Swap指令:
- 空闲让进:没有其他进程锁定时,当前进程可以进入临界区。
- 忙则等待:进程会一直尝试交换锁(自旋)。
- 有限等待:没有保证有限等待,可能会导致无限等待。
- 让权等待:同样是自旋锁,CPU 不会主动让出。
总结:
- Peterson算法 和 中断屏蔽法 都满足了前三个互斥准则,但它们的实现上,Peterson 算法依然是自旋锁,无法满足让权等待;中断屏蔽法则在单处理器系统上有效,但会阻塞其他进程的执行。
- 其余算法要么无法保证有限等待,要么通过自旋锁实现互斥,不能满足让权等待。
六、互斥锁
互斥锁(Mutex)是实现进程/线程间互斥访问共享资源的常用机制。它提供了一种通过锁定和解锁资源的方式,来确保一次只有一个线程可以进入临界区。
互斥锁的工作原理
- 当一个进程或线程需要进入临界区时,它会尝试获取互斥锁(锁定资源)。
- 如果锁未被其他进程或线程持有,当前进程可以进入临界区并锁定资源。
- 如果锁已被其他进程持有,当前进程将会被阻塞,直到锁被释放。
- 当进程离开临界区时,它会释放互斥锁,让其他等待的进程或线程继续执行。
互斥锁是否满足互斥准则分析
互斥准则 | 互斥锁 |
空闲让进 | ✅ |
忙则等待 | ✅ |
有限等待 | ✅ |
让权等待 | ✅ |
详细分析:
- 空闲让进:当没有其他线程持有锁时,任何线程都可以立即获取锁并进入临界区。因此,互斥锁满足“空闲让进”准则。
- 忙则等待:如果另一个线程持有互斥锁,任何其他线程都会被阻塞,直到锁被释放,因此满足“忙则等待”准则。
- 有限等待:互斥锁通常使用内核提供的阻塞机制,当线程无法获取锁时,它会被放入等待队列,等待锁的释放。由于使用的是阻塞机制而非自旋锁,线程会在有限时间内进入临界区,避免无限等待。因此,互斥锁满足“有限等待”准则。
- 让权等待:与自旋锁不同,互斥锁通常不进行自旋,而是主动让出 CPU,让其他线程继续执行。线程在无法获取锁时会进入等待状态,CPU资源不会被浪费。因此互斥锁满足“让权等待”准则。
互斥锁的实现代码示例(使用 Pthreads)
#include <stdio.h>#include <pthread.h>#include <unistd.h>pthread_mutex_t mutex; // 定义互斥锁void* process(void* arg) {int id = *((int*)arg);while (1) {pthread_mutex_lock(&mutex); // 获取互斥锁printf("Thread %d is in the critical section.\n", id);sleep(1); // 模拟临界区操作printf("Thread %d is leaving the critical section.\n", id);pthread_mutex_unlock(&mutex); // 释放互斥锁sleep(1); // 模拟临界区外的操作}return NULL;}int main() {pthread_t t1, t2;int id1 = 1, id2 = 2;pthread_mutex_init(&mutex, NULL); // 初始化互斥锁pthread_create(&t1, NULL, process, &id1);pthread_create(&t2, NULL, process, &id2);pthread_join(t1, NULL);pthread_join(t2, NULL);pthread_mutex_destroy(&mutex); // 销毁互斥锁return 0;}
互斥锁总结:
- 空闲让进:没有其他线程持有锁时,当前线程可以立即进入临界区。
- 忙则等待:锁被持有时,其他线程会阻塞等待。
- 有限等待:互斥锁使用的是阻塞机制,而不是自旋,因此可以保证线程在有限时间内获取锁。
- 让权等待:线程在等待锁时主动放弃 CPU,避免资源浪费。
互斥锁作为经典的进程同步和互斥机制,能够有效满足四个互斥准则,避免了自旋锁的资源浪费问题,是多线程编程中非常常用的同步手段。
七、信号量
信号量机制是一种功能较强的机制,可用来解决互斥与同步问题,它只能被两个标准的原语wait()和 signal( )访问,也可简写为 P()和 V(),或者简称 P操作和 V 操作。
原语是指完成某种功能且不被分割、不被中断执行的操作序列,通常可由硬件来实现。例如,前述的TS指令和 Swap指令就是由硬件实现的原子操作。原语功能的不被中断执行特性在单处理机上可由软件通过屏蔽中断方法实现。原语之所以不能被中断执行,是因为原语对变量的操作过程若被打断,可能会去运行另一个对同一变量的操作过程,从而出现临界段问题。
1.整型信号量
整型信号量被定义为一个用于表示资源数目的整型量S,相比于普通整型变量,对整型信号量的操作只有三种:初始化、wait 操作和signa操作。wait 操作和 signal 操作可描述为:
wait(S){ //相当于进入区while(S<=0); //若资源数不够,则一直循环等待S=S-1; //若资源数够,则占用一个资源}signal(S){ //相当于退出区S=S+1; //使用完后,就释放一个资源}
在整型信号量机制中的 wait 操作,只要信号量 S<0,就会不断循环测试。因此,该机制并未遵循“让权等待”的准则,而是使进程处于“忙等”的状态。
2.记录型信号量
记录型信号量机制是一种不存在“忙等”现象的进程同步机制。除了需要一个用于代表资源数目的整型变量 value 外,再增加一个进程链表L,用于链接所有等待该资源的进程。记录型信号量得名于采用了记录型的数据结构。记录型信号量可描述为
typedef struct {int value;struct process *L;} semaphore;相应的 wait(S)和 signal(S)的操作如下。void wait(semaphore S){ //相当于申请资源S.value--;if(S.value<0){add this process to S.L;block(S.L);}}
对信号量S的一次P操作,表示进程请求一个该类资源,因此执行S.value--,使系统中可供分配的该类资源数减1。当S.value<0时,表示该类资源已分配完毕,因此应调用block 原语进行自我阻塞(当前运行的进程:运行态一阻塞态),主动放弃CPU,并插入该类资源的等待队列S.L,可见该机制遵循了“让权等待”准则。
void signal(semaphore s){//相当于释放资源S.value++;if(S.value<=0){remove a process P from S.L;wakeup(P);}
对信号量S的一次 V操作,表示进程释放一个该类资源,因此执行S.value++,使系统中可供分配的该类资源数加1。若加1后仍是S.value<0,则表示仍有进程在等待该类资源,因此应调用 wakeup 原语将 SL中的第一个进程唤醒(被唤醒进程:阻塞态一就绪态)。
3.信号量实现互斥
为了使多个进程能互斥地访问某个临界资源,需要为该资源设置一个互斥信号量S,其初值为1(可用资源数为1),然后将各个进程访问该资源的临界区置于 P(S)和 V(S)之间。这样,每个要访问该资源的进程在进入临界区之前,都要先对S执行P操作,若该资源此刻未被访问,则本次P操作必然成功,进程便可进入自己的临界区。这时,若再有其他进程也要进入自己的临界区,由于对S执行P操作必然失败,因此主动阻塞,从而保证了该资源能被互斥访问。当访问该资源的进程退出临界区后,要对S执行V操作,以便释放该临界资源。其实现如下:
#include <stdio.h>#include <pthread.h>#include <semaphore.h>#include <unistd.h>sem_t mutex; // 定义一个信号量,用于互斥void* process(void* arg) {// P操作,申请资源(进入临界区)sem_wait(&mutex); // 当信号量的值为0时,进程会被阻塞在这里// 临界区printf("进程 %ld 进入临界区\n", (long)arg);sleep(1); // 模拟在临界区的操作// V操作,释放资源(离开临界区)printf("进程 %ld 离开临界区\n", (long)arg);sem_post(&mutex); // 增加信号量的值,允许其他进程进入临界区return NULL;}int main() {pthread_t t1, t2;// 初始化信号量,初始值为1,表示临界资源可用sem_init(&mutex, 0, 1);// 创建两个线程,模拟两个进程pthread_create(&t1, NULL, process, (void*)1);pthread_create(&t2, NULL, process, (void*)2);// 等待线程结束pthread_join(t1, NULL);pthread_join(t2, NULL);// 销毁信号量sem_destroy(&mutex);return 0;}
S 的取值范围为(-1,0,1)。当S=1时,表示两个进程都未进入临界区;当S=0时,表示有一个进程已进入临界区;当S=-1时,表示有一个进程正在临界区,另一个进程因等待而阻塞在阻塞队列中,需要被当前已在临界区中运行的进程退出时唤醒。
4.信号量实现同步
同步源于进程之间的相互合作,需要让本来异步的并发进程相互配合,有序推进。例如,进程P,和P,并发执行,由于存在异步性,因此二者推进的次序是不确定的,若P,的语句v要使用P的语句x的运行结果,则必须保证语句y一定在语句x之后执行。为了实现这种同步关系,需要设置一个同步信号量S,其初值为0(可以这么理解:刚开始没有这种资源,P,需要使用这种资源,而它又只能由P产生这种资源)。其实现如下:
#include <stdio.h>#include <pthread.h>#include <semaphore.h>#include <unistd.h>sem_t syncS; // 定义一个同步信号量void* P1(void* arg) {// 进程 P1 的操作,执行语句 xprintf("P1 执行语句 x\n");sleep(1); // 模拟语句 x 的执行时间// V 操作,通知 P2 可以继续sem_post(&syncS);printf("P1 完成,允许 P2 执行\n");return NULL;}void* P2(void* arg) {// P 操作,等待 P1 完成sem_wait(&syncS); // 只有 P1 执行完 V 操作后,P2 才能继续printf("P2 执行语句 y\n");return NULL;}int main() {pthread_t t1, t2;// 初始化同步信号量,初始值为0,表示 P2 需要等待 P1 的通知sem_init(&syncS, 0, 0);// 创建两个线程,模拟两个进程pthread_create(&t1, NULL, P1, NULL);pthread_create(&t2, NULL, P2, NULL);// 等待线程结束pthread_join(t1, NULL);pthread_join(t2, NULL);// 销毁信号量sem_destroy(&syncS);return 0;}
5.信号量实现前驱
信号量也可用来描述程序或语句之间的前驱关系。图中给出了一个前驱图,其中 S1,S2, S3,…S6是简单的程序段(只有一条语句)。
其实,每对前驱关系都是一个同步问题,因此要为每对前驱关系设置一个同步信号量,其初值均为0,在“前驱操作”之后,对相应的同步信号量执行V操作,在“后继操作”之前,对相应的同步信号量执行P操作。为保证S1→S2,S1→S3,S2-S4,S2→S5,S4→S6,S5→S6,S3→S6,的前驱关系,需分别设置同步信号量 a12,a13,a24,a25,a36,a46,a56。其实现如下:
#include <stdio.h>#include <pthread.h>#include <semaphore.h>#include <unistd.h>// 定义同步信号量sem_t a12, a13, a24, a25, a36, a46, a56;// S1 操作,作为S2和S3的前驱void* S1(void* arg) {printf("S1 执行\n");sleep(1); // 模拟任务执行sem_post(&a12); // 释放S2可以执行sem_post(&a13); // 释放S3可以执行printf("S1 完成,允许S2和S3执行\n");return NULL;}// S2 操作,作为S4和S5的前驱void* S2(void* arg) {sem_wait(&a12); // 等待S1完成printf("S2 执行\n");sleep(1); // 模拟任务执行sem_post(&a24); // 释放S4可以执行sem_post(&a25); // 释放S5可以执行printf("S2 完成,允许S4和S5执行\n");return NULL;}// S3 操作,作为S6的前驱之一void* S3(void* arg) {sem_wait(&a13); // 等待S1完成printf("S3 执行\n");sleep(1); // 模拟任务执行sem_post(&a36); // 释放S6可以执行printf("S3 完成,允许S6执行\n");return NULL;}// S4 操作,作为S6的前驱之一void* S4(void* arg) {sem_wait(&a24); // 等待S2完成printf("S4 执行\n");sleep(1); // 模拟任务执行sem_post(&a46); // 释放S6可以执行printf("S4 完成,允许S6执行\n");return NULL;}// S5 操作,作为S6的前驱之一void* S5(void* arg) {sem_wait(&a25); // 等待S2完成printf("S5 执行\n");sleep(1); // 模拟任务执行sem_post(&a56); // 释放S6可以执行printf("S5 完成,允许S6执行\n");return NULL;}// S6 操作,需等待S3、S4、S5完成void* S6(void* arg) {sem_wait(&a36); // 等待S3完成sem_wait(&a46); // 等待S4完成sem_wait(&a56); // 等待S5完成printf("S6 执行\n");sleep(1); // 模拟任务执行printf("S6 完成\n");return NULL;}int main() {pthread_t t1, t2, t3, t4, t5, t6;// 初始化同步信号量,初值为0sem_init(&a12, 0, 0);sem_init(&a13, 0, 0);sem_init(&a24, 0, 0);sem_init(&a25, 0, 0);sem_init(&a36, 0, 0);sem_init(&a46, 0, 0);sem_init(&a56, 0, 0);// 创建线程,模拟各个操作pthread_create(&t1, NULL, S1, NULL);pthread_create(&t2, NULL, S2, NULL);pthread_create(&t3, NULL, S3, NULL);pthread_create(&t4, NULL, S4, NULL);pthread_create(&t5, NULL, S5, NULL);pthread_create(&t6, NULL, S6, NULL);// 等待线程结束pthread_join(t1, NULL);pthread_join(t2, NULL);pthread_join(t3, NULL);pthread_join(t4, NULL);pthread_join(t5, NULL);pthread_join(t6, NULL);// 销毁信号量sem_destroy(&a12);sem_destroy(&a13);sem_destroy(&a24);sem_destroy(&a25);sem_destroy(&a36);sem_destroy(&a46);sem_destroy(&a56);return 0;}
八、管程(monitor)
在信号量机制中,每个要访问临界资源的进程都必须自备同步的PV操作,大量分散的同步操作给系统管理带来了麻烦,且容易因同步操作不当而导致系统死锁。于是,便产生了一种新的进程同步工具——管程。管程的特性保证了进程互斥,无须程序员自己实现互斥,从而降低了死锁发生的可能性。同时管程提供了条件变量,可以让程序员灵活地实现进程同步。
1.管程的组成
①管程的名称;
②局部于管程内部的共享数据结构说明:
③对该数据结构进行操作的一组过程(或函数);
④对局部于管程内部的共享数据设置初始值的语句。
管程的定义描述举例如下:
Monitor Demo{//①定义一个名称为 Demo 的管程//②定义共享数据结构,对应系统中的某种共享资源共享数据结构S;//④对共享数据结构初始化的语句init code (){S=5; //初始资源数等于5}take away(){//③过程 1:申请一个资源对共享数据结构x的一系列处理;S--; //可用资源数-1…}give back(){ //③)过程 2:归还一个资源对共享数据结构x的一系列处理;S++; //可用资源数+1…}}
2.条件变量
当一个进程进入管程后被阻塞,直到阻塞的原因解除时,在此期间,如果该进程不释放管程,那么其他进程无法进入管程。为此,将阻塞原因定义为条件变量condition。通常,一个进程被阳塞的原因可以有多个,因此在管程中设置了多个条件变量。每个条件变量保存了一个等待队列,用于记录因该条件变量而阻塞的所有进程,对条件变量只能进行两种操作,即wait 和signal。
x . wait: | 当x对应的条件不满足时,正在调用管程的进程调用 x.wait 将自己插入x条件的等待队列,并释放管程。此时其他进程可以使用该管程。 |
x . signal | x对应的条件发生了变化,则调用x.signal,唤醒一个因x条件而阻塞的进程。 |
下面给出条件变量的定义和使用:
monitor Demo{共享数据结构S;condition x; //定义一个条件变量xinit code(){ ... }take away(){if(S<=0)x.wait(); //资源不够,在条件变量x上阻塞等待资源足够,分配资源,做一系列相应处理;}give back(){归还资源,做一系列相应处理;if(有进程在等待)x.signal();//唤醒一个阻塞进程}}
条件变量和信号量的对比:
相似点: | 条件变量的 wait/signal 操作类似于信号量的 P/V 操作,可以实现进程的阻塞/唤醒。 |
不同点: | 条件变量是“没有值”的,仅实现了“排队等待”功能; |
而信号量是“有值”的,信号量的值反映了剩余资源数,而在管程中,剩余资源数用共享数据结构记录。 |
相关文章:

进程的同步与互斥
目录 一、进程同步 二、进程互斥 1.临界资源访问代码: ①进入区 ②临界区 ③退出区 ④剩余区 注: 2.互斥准则: ①.空闲让进。 ②.忙则等待。 ③.有限等待。 ④.让权等待。 三、进程互斥的软件实现方法 1.单标志法 2.双标志先…...

基础的八股
JS this 全局:this指向window 函数:this指向window 对象:this指向调用它的 get、post的区别 1、写的地方不同:get在地址栏里 地址栏有多长就只能写多少、post在请求体里 没有上限 2、关于回退和刷新:get回退和刷新没问…...

使用Python从头开始创建PowerPoint演示文稿
目录 一、环境搭建与基础知识 1.1 环境搭建 1.2 基础知识 二、创建演示文稿对象 三、添加幻灯片 3.1 选择幻灯片布局 3.2 设置幻灯片内容 3.2.1 设置标题和副标题 3.2.2 添加文本内容 3.2.3 插入图片 3.2.4 插入图表 四、高级应用:批量生成演示文稿 4.…...

【C++ Primer Plus习题】15.4
大家好,这里是国中之林! ❥前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站。有兴趣的可以点点进去看看← 问题: 解答: main.cpp #include <iostream> #include "sales.h"…...

Pipeline Scheduling(UVA 690)
网址如下: Pipeline Scheduling - UVA 690 - Virtual Judge (vjudge.net) (第三方网站) 噫,好!我中了! 这题还是有点折磨的,刚开始我只会递归下一个程序运行的时间(范围在1~n&…...

萤石举办2024清洁机器人新品发布会 多维智能再造行业标杆
导言:作为智慧生活守护者,萤石今日发布了两款清洁机器人,AI扫拖机器人RS20 Pro Ultra 和AI洗地机器人RX30 Max ,标志着萤石在智能清洁领域的全新突破。RS20 Pro Ultra基于CutFree 2.0内切割滚刷专利,有效解决毛发缠绕难…...

企业级Ansible自动化运维项目案例:实战与技巧
在企业级的IT运维中,自动化已成为提高效率、减少人为错误和保证服务一致性的关键手段。Ansible作为一种简单但功能强大的自动化工具,广泛应用于配置管理、应用程序部署、任务自动化和IT编排。本文将通过一个企业级的Ansible自动化运维项目案例࿰…...

JavaSE-易错题集-005
1. 下面有关java object默认的基本方法,说法错误的是? A equals(Object obj) 指示某个其他对象是否与此对象“相等” B copy() 创建并返回此对象的一个副本 C wait() 导致当前的线程等待,直到其他线程调用此对象的 notify() 方法或 notifyA…...

决策树模型的可解释性
我们首先介绍一下一个比较简单的机器学习模型,其在设计之初就已经有了比较好的可 解释性,这个模型就是决策树模型。决策树相较于线性的模型,它是更强大的模型。而决策树 的另外一个好处,相较于深度学习它具有良好的可解释性。比如…...

2. geoserver 发布postgis数据
1. 新建工作空间 2. 新建存储空间 3. 新建图层 4. 切片图层 5. 查看发布的图层...

【渗透测试】——Brup Suite平台安装
📖 前言:Burp Suite 是用于攻击 web 应用程序的集成平台。它包含了许多Burp工具,这些不同的burp工具通过协同工作,有效的分享信息,支持以某种工具中的信息为基础供另一种工具使用的方式发起攻击。 它主要用来做安全性…...

redis:全局ID生成器实现
问题:订单id不能设置为自增长的原因 id的规律性太明显, 受订单的数据量限制:若数据量过大,需要多张表存储,若自增会导致id重复 全局ID生成器:在分布式系统中用来生成全局唯一ID的工具 ID的组成: 符号位…...

jenkins工具的介绍和gitlab安装
使用方式 替代手动,自动化拉取、集成、构建、测试;是CI/CD持续集成、持续部署主流开发模式中重要工具;必须组件 jenkins-gitlab,代码公共仓库服务器(至少6G内存);jenkins-server,需…...

【从0开始在CentOS 9中安装Tomcat】
从0开始在CentOS 9中安装Tomcat 1. 安装 Java(Tomcat 需要 Java 环境)2. 下载并安装 Tomcat3. 配置 Tomcat4. 启动 Tomcat5. 配置 Tomcat 为开机自启动6. 验证 Tomcat 运行状态7. 允许防火墙开放 8080 端口(可选) 要在 Linux 上安…...

学习Vue3的第五天
目录 API对比 shallowRef 与 shallowReactive 对比总结 使用场景 总结 readonly 与 shallowReadonly 对比总结 使用场景 总结 toRaw 与 markRaw 对比总结 使用场景 总结 customRef 应用场景 总结 示例:异步数据获取 Vue3新组件 Teleport Suspen…...

Python 类中使用 cursor.execute() 时语法错误的解决方法
在 Python 类中使用 cursor.execute() 时,出现语法错误(如 SyntaxError 或 SQL 语法相关错误)通常是因为 SQL 语句格式不正确、占位符使用不当,或参数传递方式不符合预期。以下是解决此类问题的常见方法和建议。 问题背景 在 Pyt…...

怎么选择靠谱AI论文生成工具?看完我的试用都会明白!
2024年上半年开始AI论文写作工具开始火了,层出不穷!作为一个经常需要写论文的懒人,我非常好奇这些AI工具的实际效果到底怎么样?为了测试不同工具的实力,我对他们都进行了试用,发现了一些意想不到的结果....…...

Java 每日一刊(第3期):Hello World
文章目录 前言Hello World程序是如何执行的Hello World 里有什么本期小知识 阳光洒进窗台,花香伴着书香,静谧而温暖,仿佛时光停驻。 前言 这里是分享 Java 相关内容的专刊,每日一更。 本期将为大家带来以下内容: “…...

git一个项目关联多个远程仓库
一行代码就行: git remote set-url origin [想要关联的远程仓库地址]想要关联哪个就切换哪个 或者不用每次切换,集中管理: Git->Manage Remotes 点击“”,填入Name和想要关联的远程库地址 每次push时执行命令 git push [为…...

衡石分析平台使用手册-部署前准备
部署前准备 1.根据版本获取 k8s 部署配置文件。 安装版本部署文件组件依赖3.xk8s-yamlmetadb、engine、hengshi zookeeper4.0.xk8s-yamlmetadb、engine、hengshi、minio、zookeeper4.1.xk8s-yamlmetadb、engine、hengshi、minio、redis、flink、zookeeper4.2.xk8s-yamlmeta…...

AI大模型全栈工程师课程笔记 - RAG 检索增强生成
文章目录 \1. RAG\2. 构建流程 2.1 文档加载与切分2.2 传统检索引擎2.3 LLM接口封装2.4 构建prompt \3. 向量检索\4. 向量数据库\5. 基于向量检索的RAG\6. 进阶知识 6.1 文本分割粒度6.2 检索后再排序6.3 测试 1. RAG RAG(Retrieval Augmented Generation&#…...

【时时三省】c语言例题----华为机试题<进制转换>
山不在高,有仙则名。水不在深,有龙则灵。 ----CSDN 时时三省 1,题目 HJ5 进制转换 描述 写出一个程序,接受一个十六进制的数,输出该数值的十进制表示。 数据范围:保证结果在 1≤n≤231−1 1≤n≤231−1…...

根据NVeloDocx Word模板引擎生成Word(四)
前面介绍了《E6低代码开发平台》的Word模版引擎NVeloDocx,实现了表单的基本字段、子表、单张图片、二维码、条形码怎么基于NVelocity脚本输出到Word文件,都是些比较简单且常用的需求。 本篇介绍怎么基于NVeloDocx在Word中插入图表,目前只支持…...

C++笔记---stack和queue
1. stack的介绍及重要接口 stack---栈,是一种“先进后出,后进先出”的数据结构。 此处的stack是STL库中定义的一个类模板,用于实例化出存储各种类型数据的栈。 bool empty() const;判断栈是否为空(空true/非空false)size_t size() const;返…...

springboot Rabbit MQ topic 配置文件绑定队列和交换机
Spring Boot 中如何将队列和交换机绑定(含实例讲解) 在使用 Spring Boot 开发高并发的秒杀系统或者其他场景时,RabbitMQ 是常用的消息队列中间件之一。本文将详细讲解如何在配置类中通过代码将队列与交换机绑定,并指定路由键来实…...

Visual Studio 2019密钥
Visual Studio 2019 Enterprise(企业版):BF8Y8-GN2QH-T84XB-QVY3B-RC4DF Visual Studio 2019 Professional(专业版):NYWVH-HT4XC-R2WYW-9Y3CM-X4V3Y...

【三元组枚举中点】【树状数组】个人练习-Leetcode-1395. Count Number of Teams
题目链接:https://leetcode.cn/problems/count-number-of-teams/description/ 题目大意:给一个数组rating[],求符合以下任一条件的三元组i, j, k的个数 rating[i] < rating[j] < rating[k]rating[i] > rating[j] > rating[k] …...

Anaconda 中遇到CondaHTTPError: HTTP 404 NOT FOUND for url的问题及解决办法
最近在跑一个开源项目遇到了以下问题,查了很多资料都大(抄)同(来)小(抄)异(去)的,解决不了根本问题,费了很大的劲终于得以解决,记录如…...

数据库系统 第51节 数据库事务管理
数据库事务管理是数据库管理系统(DBMS)中用于确保数据完整性和一致性的一组机制。事务是一组不可分割的操作序列,这些操作要么全部成功,要么全部失败。以下是数据库事务管理的关键组成部分的详细叙述: 1. 事务隔离级别…...

分解+优化+组合+对比!核心无忧!VMD-SSA-Transformer-LSTM多变量时间序列光伏功率预测
分解优化组合对比!核心无忧!VMD-SSA-Transformer-LSTM多变量时间序列光伏功率预测 目录 分解优化组合对比!核心无忧!VMD-SSA-Transformer-LSTM多变量时间序列光伏功率预测效果一览基本介绍程序设计参考资料 效果一览 基本介绍 1.…...