进程并发编程基础
前言:看了操作系统并发编程的基础,做个笔记并用 C 实现常见的一种并发编程的模型——消费者、生产者模型
进程之间的关系
第一种是竞争关系
系统中的多个进程之间彼此无关,它们并不知道其他进程的存在,并且也不受其他进程执行的影响。因而, 必然要出现多个进程竞争资源的问题。当多个进程竞争共享硬设备、存储器、处理器 和文件等资源时,操作系统必须协调好进程对资源的争用。
进程的互斥(mutual exclusion )是解决进程间竞争关系(间接制约关系)的手段。 进程互斥指若干个进程要使用同一共享资源时,任何时刻最多允许一个进程去使用,其他要使用该资源的进程必须等待,直到占有资源的进程释放该资源。
第二种是协作关系
某些进程为完成同一任务需要分工协作,由于合作的每一个进程都是独立地以不可预知的速度推进,这就需要相互协作的进程在某些协调点上协 调各自的工作。
进程的同步(Synchronization)是解决进程间协作关系(直接制约关系)的手段。进程同步指两个以上进程基于某个条件来协调它们的活动。一个进程的执行依赖于另一个协作进程的消息或信号,当一个进程没有得到来自于另一个进程的消息或信号时则需等待,直到消息或信号到达才被唤醒。
不难看出,进程互斥关系是一种特殊的进程同步关系,即逐次使用互斥共享资源,也是对进程使用资源次序上的一种协调。
如何实现互斥
无论是竞争还是协作,都必须实现互斥。
而实现互斥的关键在于:进程 A 正在使用的临界资源,哪怕在使用的过程中被中断了,其他进程的临界区都不能动进程 A 的资源。
硬件方式实现互斥
用硬件的方式增加一条汇编指令,比如 TestAndSet
伪代码如下
// 主程序
global lock = false
repeate {nothing} until TestAndSet(&lock)
// 临界区代码
// do someting
// 临界区结束
lock = false
这里的 TestAndSet 使用汇编指令实现的,也就是他不能被中断。lock 是全局变量,所有的进程都能访问到
实现的功能就是:检查 lock 是否是 false,如果是 false 就把 lock 设为 true。如果 lock 是 false。就不会执行repeate {nothing}
。
有的进程执行的时候,lock 是 true。就会一直执行 repeate {nothing}
,直到 CPU 时间片结束,结束调用。这样就实现了互斥。
但是,这样有一个很大的缺点就是忙等,我们不想要忙等,我们想要 lock = true
的时候进程被阻塞,然后等待调用
用信号量(semaphore)实现互斥 重点!
semaphore 最重要的是两个函数:
-
wait(semaphore* s)
-
signal(semaphore* s)
这是系统给的原语。原语和其他函数最大的区别就是:它虽然也是一系列指令构成,但是在执行的过程中不可中断
wait 和 signal 的伪代码如下:
wait(semaphore* s) {
if(s.count-- < 0) {
// 阻塞
}
}
signal(semaphore* s) {
if(s.count++ <= 0) {
// 调用某个被阻塞的进程
call();
}
}
那怎么用这两个函数实现互斥呢?我们在访问临界资源的时候会先执行 wait 函数如果 (s.count-- < 0)。则被阻塞,而不是忙等。否则就执行临界区代码(资源够用,不需要阻塞)。
而执行完临界区以后,会执行 signal 函数。如果 (s.count++ <= 0) 。则去唤醒一个被阻塞的函数,否则啥也不做,仅仅加一(没有被阻塞的进程)。
通过一个实例来理解这一个过程
#include <stdio.h>
#include <stdlib.h>
#include <semaphore.h>
#include <unistd.h>
sem_t s;
int main(int argc, char const *argv[])
{
int value = 0;
sem_init(&s, 0, 10);
sem_getvalue(&s, &value);
printf("initialization s.count = %d\n", value);
// wait
sem_wait(&s);
sem_getvalue(&s, &value);
printf("after wait s.count = %d\n", value);
// signal
sem_post(&s);
sem_getvalue(&s, &value);
printf("after signal s.count = %d\n", value);
sem_destroy(&s);
return 0;
}
输出:
initialization s.count = 10
after wait s.count = 9
after signal s.count = 10
C 实例
我们通过两个经典模型,来深入理解进程并发编程
消费者、生产者模型
该问题描述了共享固定大小缓冲区的两个进程——即所谓的“生产者”和“消费者”——在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。
#include <stdio.h>
#include <stdlib.h>
#include <semaphore.h>
#include <unistd.h>
#define SIZE 1 /*buffer size*/
sem_t empty;
sem_t full;
sem_t mutex;
int buffer[SIZE] = {0};
void producer(int idx, int value) {
sem_wait(&full);
sem_wait(&mutex);
buffer[idx] = value;
sem_post(&mutex);
sem_post(&empty);
}
void consumer(int idx) {
sem_wait(&empty);
sem_wait(&mutex);
printf("从 buffer[%d] 取出 %d \n", idx, buffer[idx]);
buffer[idx] = -1;
sem_post(&mutex);
sem_post(&full);
}
int main(int argc, char const *argv[]) {
int value;
// 多个生产者和消费者,只有一个进程能够拥有 buffer,这是所有进程的互斥
sem_init(&mutex, 0, 1);
// 生产者在满的时候不能生产
sem_init(&full, 0, SIZE);
// 消费者不能在空的时候消费
sem_init(&empty, 0, 0);
// 假设有四个进程
for(int i = 0; i < 4; i++) {
int p = fork();
if(p == 0) {
switch (i)
{
case 0:
producer(0, 10);
break;
case 1:
producer(0, 100);
break;
case 2:
consumer(0);
break;
case 3:
consumer(0);
break;
default :
// producer(0, 10);
break;
}
} else {
printf("我是主进程:%d \n", p);
}
}
sem_destroy(&mutex);
sem_destroy(&empty);
sem_destroy(&full);
return 0;
}