进程通信那些事

108 阅读4分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第2天,点击查看活动详情

进程通信引发的问题

  • 我们知道同一个进程的多个线程会共享该进程的数据,进程与进程之间是不会共享数据的。
  • 因此进程之间不知道对方对数据的修改的,当两个进程同时对一个数据进行操作时就有可能会出大问题了。
  • 这时就会产生 竞争条件 ,要阻止多个进程同时读写共享数据,就需要用 互斥 来解决这个问题,那么如何设计互斥条件呢?
  • 我们把访问共享内存的的程序片段称为 临界区域
  • 解决临界问题好的方案需满足:①任何两个进程不能同时处于临界区。②不应对 CPU 的速度和数量做任何假设。③临界外运行的程序不得阻塞其他进程。④不得使进程无限期等待进入临界区。

忙等待的互斥方案

严格轮换法

  • 见下方代码,整形变量 turn 初始值为 0 ,用于记录该那个进程进入临界区。像这种用于忙等待的锁称为 自旋锁 。但是当两个进程的速度相差较大时,较快的进程就会在 while 循环中等待,知道慢的进程把 turn 设置为对方进程。就违反了条件③临界外运行的程序不得阻塞其他进程。
while () {
    while (turn != 0);  // 循环
    critical_region();
    turn = 1;
    noncritical_region();
}

while () {
    while (turn != 1);  // 循环
    critical_region();
    turn = 0;
    noncritical_region();
}

Peterson 解法

  • 开始 0 调用 enter_region(),如果 1 不去竞争的话,0 会一直执行到结束。如果 0 和 1 都调用 enter_region(),则谁先执行到 turn = process,并执行到 while 循环,谁就先进入临界区。如果有一个进入到临界区时,另外一个进程会一直等到它离开临界区时,才能够进入临界区。
#define FALSE 0
#define TRUE 1
#define N 2

int turn;
int interested[N];

void enter_region(int process) {
    int other = 1 - process;             // 另一个进程
    interested[process] = TRUE;          // 表示感兴趣
    turn = process;                      // 设置标识
    while (turn == process && interested[other] == TRUE);
}

void leave_region(int process) {
    interested[process] = FALSE;         // 表示离开临界区
}

生产者 - 消费者方案

采用信号量

  • 这里我们使用的 down() 和 up() 操作是具有原子性的,它能够保证读和写操作是不可分割的,而处理器的 TSL 和 XCHG 指令能够满足我们的这些需求。
  • 这里我们用 mutex,empty,full 三个信号量。分别表示是否进入临界区、空槽数量、满槽数量。这些信号量也是能够实现同步的。
#define N 100
typedef int semaphore;    // 信号量特殊整数类型
semaphore mutex = 1;      // 控制对临界区域的访问
semaphore empty = N;      // 空槽数量
semaphore full = 0;       // 满槽数量

void producer(void) {
    int item;
    while (TRUE) {
        item = produce_item();
        down(&empty);          // 如果为 0 就会进入睡眠,否则空槽数量 - 1
        down(&mutex);          // 进入临界区域
        insert_item(item);     // 将一个单位数据放入缓冲区空槽
        up(&mutex);            // 离开临界区域
        up(&full);             // 满槽数量 + 1
    }
}

void consumer(void) {
    int item;
    while (TRUE) {
        down(&full);          // 如果为 0 就会进入睡眠,否则满槽数量 - 1
        down(&mutex);         // 进入临界区域
        item = remove_item();
        up(&mutex);           // 离开临界区域
        up(&empty);           // 空槽数量 + 1
        consumer_item(item);  // 从缓冲区中取出一个单位数据
    }
}

采用互斥量

  • 这里使用的俩个互斥量分别为:解锁加锁,而互斥量又分为:①快速用户区互斥量 futex。②pthread 中的互斥量。
  • 快速用户区互斥量:它实现了基本的锁,但是在非必要时不陷入内核,从而改善了性能(陷入内核的花销很大)。
  • pthread 互斥量:它时一个提供同步线程的函数库。
#define MAX 1000000000
pthread_mutex_t the_mutex;
pthread_cond_t condc;
pthread_cond_t condp;
int buffer = 0;              // 缓冲区

void *producer(void *ptr) {
    for (int i = 0; i <= MAX; i++) {
        pthread_mutex_lock(&the_mutex);    // 互斥使用缓冲区
        whule (buffer != 0) pthtread_cond_wait(&condp,&the_mutex);
        buffer = i;                        // 数据放入缓冲区
        pthread_cond_signal(&condc);       // 唤醒消费者
        pthread_mutex_unlock(&the_mutex);  // 释放缓冲区
    }
    pthread_exit(0);
}

void *consumer(void *ptr) {
    for (int i = 0; i <= MAX; i++) {
        pthread_mutex_lock(&the_mutex);    // 互斥使用缓冲区
        whule (buffer == 0) pthtread_cond_wait(&condc,&the_mutex);
        buffer = 0;                        // 从缓冲区取出数据
        pthread_cond_signal(&condp);       // 唤醒生产者
        pthread_mutex_unlock(&the_mutex);  // 释放缓冲区
    }
    pthread_exit(0);
}

总结

  • 通过对进程通信解决算法的了解与认识,我们就可以加深对一些比较经典的算法的理解。其中的奥妙之处也会不经意之间出现在我们写的代码中!