百度收录网站链接,168分类信息发布网,自己怎么制作网站,国外如何建立个人网站目录
一、进程同步
二、进程互斥
1.临界资源访问代码#xff1a;
①进入区
②临界区
③退出区
④剩余区
注#xff1a;
2.互斥准则#xff1a;
①.空闲让进。
②.忙则等待。
③.有限等待。
④.让权等待。
三、进程互斥的软件实现方法
1.单标志法
2.双标志先…目录
一、进程同步
二、进程互斥
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.hint 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.hint 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.hint 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.hint 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.hpthread_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(S0); //若资源数不够则一直循环等待SS-1; //若资源数够则占用一个资源}signal(S){ //相当于退出区SS1; //使用完后就释放一个资源}
在整型信号量机制中的 wait 操作只要信号量 S0就会不断循环测试。因此该机制并未遵循“让权等待”的准则而是使进程处于“忙等”的状态。
2.记录型信号量
记录型信号量机制是一种不存在“忙等”现象的进程同步机制。除了需要一个用于代表资源数目的整型变量 value 外再增加一个进程链表L用于链接所有等待该资源的进程。记录型信号量得名于采用了记录型的数据结构。记录型信号量可描述为
typedef struct {int value;struct process *L;} semaphore;相应的 wait(S)和 signal(S)的操作如下。void wait(semaphore S){ //相当于申请资源S.value--;if(S.value0){add this process to S.L;block(S.L);}}
对信号量S的一次P操作表示进程请求一个该类资源因此执行S.value--使系统中可供分配的该类资源数减1。当S.value0时表示该类资源已分配完毕因此应调用block 原语进行自我阻塞(当前运行的进程:运行态一阻塞态)主动放弃CPU并插入该类资源的等待队列S.L可见该机制遵循了“让权等待”准则。
void signal(semaphore s){//相当于释放资源S.value;if(S.value0){remove a process P from S.L;wakeup(P);}
对信号量S的一次 V操作表示进程释放一个该类资源因此执行S.value使系统中可供分配的该类资源数加1。若加1后仍是S.value0,则表示仍有进程在等待该类资源因此应调用 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.hsem_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)。当S1时表示两个进程都未进入临界区;当S0时表示有一个进程已进入临界区;当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.hsem_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.信号量实现前驱
信号量也可用来描述程序或语句之间的前驱关系。图中给出了一个前驱图,其中 S1S2, S3,…S6是简单的程序段(只有一条语句)。 其实每对前驱关系都是一个同步问题因此要为每对前驱关系设置一个同步信号量其初值均为0在“前驱操作”之后对相应的同步信号量执行V操作在“后继操作”之前对相应的同步信号量执行P操作。为保证S1→S2S1→S3S2-S4S2→S5S4→S6S5→S6S3→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 (){S5; //初始资源数等于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(S0)x.wait(); //资源不够在条件变量x上阻塞等待资源足够分配资源做一系列相应处理;}give back(){归还资源做一系列相应处理;if(有进程在等待)x.signal();//唤醒一个阻塞进程}}
条件变量和信号量的对比 相似点 条件变量的 wait/signal 操作类似于信号量的 P/V 操作可以实现进程的阻塞/唤醒。 不同点 条件变量是“没有值”的仅实现了“排队等待”功能; 而信号量是“有值”的,信号量的值反映了剩余资源数而在管程中剩余资源数用共享数据结构记录。