美文网首页
《操作系统概念精要》基本概念整理之进程同步篇(一)

《操作系统概念精要》基本概念整理之进程同步篇(一)

作者: 小pb | 来源:发表于2019-10-30 08:32 被阅读0次

同步

在之前的学习中,了解了进程能与系统内其他执行进程相互影响。协作进程之间或能直接共享逻辑地址空间(代码和数据),或能通过文件或者消息来共享数据。前一种是通过线程来实现的。

这里以进程之间最常见的同步问题,生产者-消费者(也叫有界缓冲区)问题来开始讨论吧。
问题描述:假设有一个固定大小的缓冲区,生产者生产数据写入,消费者取出数据进行消费。当缓冲区满的时候,生产者必须等待;当缓冲区为空时,消费者必须等待。

对于这个问题,我们进行深入分析,首先有一个必须共享的缓冲区。所以先进行下面的定义。

   typedef struct {
     ...
   } Item;
   #define BUFFER_SIZE 10
   Item  buffer[BUFFER_SIZE];
   int count = 0;    // 缓冲区中的数据量
   int in = 0;     // 用来标识生产者存放下一个数据位置
   int out = 0;    // 用来标识消费者取出下一个数据位置

我们假设缓冲区的数据为BUFFER_SIZE,则我们可以得到这样的伪代码:

// producer                                                                              
while (true) {                                                                           
    A = producer();                                                                      
                                                                                         
    // 缓冲区满,等待                                                                     
    while (BUFFER_SIZE == count) ;                                                       
                                                                                         
    buffer[in] = A;                                                                      
    in = (in + 1) % BUFFER_SIZE;                                                         
    count++;                                                                             
}                                                                                        
                                                                                                                                                                                 
// cosumer                                                                               
while (true) {                                                                           
    // 缓冲区空,等待                                                                     
    while (0 == count) ;                                                                 
                                                                                         
    B = buffer[out];                                                                     
    out = (out + 1) % BUFFER_SIZE;                                                       
    count--;                                                                             
} 

虽然以上代码在各自的程序中各自正确,但是在并发执行时,可能会存在问题,因为count 这个数据是两个程序共享的。
"count++" 可以按照机器语言分解:

    regisetr1 = count;
    register1 = register + 1 
    count = register;

相应的"count--" 可以分解为

    register2 = count ; 
    register2 =  register2 - 1;
    count = register2;

在CPU交替执行的时候,会产生三种结果。

producer_cosumer.png

这样的并发执行,我们得到count = 4; 如果T4,T5交换顺序,那么将得到6;

像这种,多个进程并发访问和操作同一数据并且执行结果与特定顺序有关,称为竞争条件

临界区问题

临界区: 假设某个系统有n个进程{P0, P1, P2 .... Pn-1} 每个进程有一段代码,进程在执行的过程中,可能修改公共变量,更新一个表,写一个文件。这段代码叫做临界区。

当有进程在临界区执行的时候,其他进程不允许在它们的临界区内执行。一般的临界区,我们会根据他们的结构分为进入区(entry section)退出区(exit section)临界区(ctitical section)剩余区(remainder section)。如图所示:

ctitical_section临界区结构.png

临界区问题的解决方案需要满足三个要求:

  1. 互斥:如果进程Pi在其临界区内执行,那么其他进程不能再其临界区执行。
  2. 进步: 如果没有进程在其临界区,并且有进程需要进入临界区,那么只有那些不在剩余区内执行的进程可以参选,以便确定谁能下次进入临界区,而且这种选择不能无限推迟。
  3. 有限等待: 从一个进程作出进入临界区的请求直到这个请求允许为止,其他进程进入临界区的次数具有上限。

PeterSon算法

针对临界区问题,PeterSon给出了自己的算法,只要适用于两个线程交错执行临界区和剩余区。
假设有两个线程,P0和P1,这里只有两个线程,所以这里用i,j来表示,如果线程Pi表示一个线程,那么Pj表示另一个线程,两个线程共享数据

int turn;
bool flags[2];

变量turn表示那个线程程可以进入到临界区,如果turn == 0;表示进程P0可以进入缓冲区。
数组flags表示那个线程可以进入临界区。判断一个线程能否进入缓冲区需要 其他线程不在临界区。比如Pi线程需要判断,Pj在不在临界区内。他需要两个条件来判断 flags[j] 和turn == j; 所以可以得到这样的伪码结构:


peterson.png

为了进入临界区,进程Pi 首先设置flags[i] = true;表示第i个线程准备进入临界区。
turn == j; 这个是一个在执行过程中,根据操作系统调度的顺序,来选择的线程的一个值。因为假设P0和P1两个线程同时执行到了这里,并且都对flags值进行了设置,那么根据操作系统的调度,有可能两个同时被执行了,虽然这里会有不同的结果,但是最后的 turn 只能有一个,要么为0,要么为1 。(根据语义,我们可以看出,这个值其实可以 可以用turn == i,我读到这里的一个疑问,但是请看下面的一个条件判断。)

while (flags[j] && turn == j ) ; 这个判断主要是看另外一个线程有没有在临界区,如果在临界区,那么忙等待它退出;(上一步的j不能被替换,因为他会使 这个循环直接退出)。
flags[i] = false; 当退出临界区的时候,设置这个值,会使另外一个忙等待的线程跳出循环,进入到临界区。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>


// 线程同步之临界区问题
// PeterSon算法

// 共享count数据
int count = 0;

// turn表示哪个进程可以进入临界区,
// turn == i; 表示第i个线程允许在临界区内执行。
int turn = 0;
// 数组flags 表示哪个进程准备进入临界区。
// flags[i] 为true, 表示第i个线程准备进入临界区。
bool flags[2];

void* process0(void* arg) {
    while (true) {
        // 第0个线程准备进入临界区
        flags[0] = true;
        // 如果两个线程同时进去,那么turn 会几乎在同时设置为0和1;
        // 但是只有一个赋值语句可以成功,因为一个会被另外一个覆盖。
        // 变量turn 表示最后会去哪一个。
        // 这里turn 不能设置为0,因为下面的while的第二个条件直接跑走了。
        // 默认应该是进行忙等待的
        turn = 1;

        while (flags[1] && turn == 1);
        // 临界区代码
        {
            count++;
            sleep(1);
            printf("process 0 in 临界区. count=%d\n", count);
        }
        //
        flags[0] = false;
    }
}

void* process1(void* arg) {
    while (true) {
        // 第1个线程准备进入临界区
        flags[1] = true;
        // 如果两个线程同时进去,那么turn 会几乎在同时设置为0和1;
        // 但是只有一个赋值语句可以成功,因为一个会被另外一个覆盖。
        // 变量turn 表示最后会去哪一个。
        turn = 0;

        while (flags[0] && turn == 0);
        // 临界区代码
        {
            count++;
            sleep(1);
            printf("process 1 in 临界区. count=%d\n", count);
        }
        //
        flags[1] = false;
    }
}

int main() {

    pthread_t  t0, t1;
    flags[0] = flags[1] = false;

    int err;
    turn = 0;
    err = pthread_create(&t0, NULL, process0, NULL);

    if (err != 0)
        exit(-1);

    err = pthread_create(&t1, NULL, process1, NULL);

    if (err != 0)
        exit(-1);

    pthread_join(t0, NULL);
    pthread_join(t1, NULL);
    return 0;
}

相关文章

网友评论

      本文标题:《操作系统概念精要》基本概念整理之进程同步篇(一)

      本文链接:https://www.haomeiwen.com/subject/bpinvctx.html