进程与线程(三)——进程/线程间通信

  1. 云栖社区>
  2. 博客>
  3. 正文

进程与线程(三)——进程/线程间通信

王小闹儿 2018-12-04 19:35:16 浏览270
展开阅读全文

在用户空间中创建线程

 

用库函数实现线程(《现代操作系统》 P61)

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

#define NUMBER_OF_THREADS 10

void *print_hello_world(void* tid){
   printf("hello world, greeting from thread %d\n", tid);
   pthread_exit(NULL);
}

int main(int argc, char *argv[]){
   pthread_t threads[NUMBER_OF_THREADS];
   int status=0;

   for(int i=0; i<NUMBER_OF_THREADS; i++){
      printf("Main hrer, Creating thread %d\n", i);
      status=pthread_create(&threads[i], NULL, print_hello_world, (void *)i);

      if(status !=0){
         printf("Oops.thread_creat returned error code %d\n", status);
         exit(-1);
      }
   }
   exit(NULL);
}
~             

在centos下对上述代码进行编译:

g++ -lpthread -o 第一个线程创建的例子  第一个线程创建的例子.cpp 

生成可执行文件,运行结果:

 

在用户空间管理线程时,每个进程需要一个专用的线程表(thread table),用来跟踪该进程中的线程。这个表与内核中的进程表类似。当一个线程转换到就绪态或阻塞态时,在线程表中存放重启该线程所需的信息,与内核在进程表中存放进程的信息完全一样。

 

 

 

进程间通信

 

竞争条件(race condition)(《现代操作系统》P67)

两个或多个进程读写某些共享数据,而最后的结果取决于进程运行的精确时序

 

引起同步问题的因素:

1、时钟中断

当一个进程测试锁的值为0时,此时发生一次时钟中断,CPU切换到另一个进程,另一个进程读取到锁的值为0,因此将锁的值改为1并进入临界区,再次发生时钟中断,CPU切换回来,刚刚读取锁的值为0的进程也将锁的值赋值为1并进入临界区。

 

2、信号量丢失

当消费者从缓冲区中取出一个元素之后,count的值变为0,此时此时调度程序决定暂停消费者并启动生产者。生产者想缓冲区中加入一个元素,此时生产者发现count的值变成了1,他推断由于各个count为0,所以消费者一定在睡眠,于是生产者迪奥哟经wakeup来唤醒消费者。

但是此时消费者并没有睡眠,因此wakeup信号丢失。当消费者运行时,他上一次读到的count值为0,因此进入睡眠。生产者迟早会填满缓冲区,然后睡眠。这样两个进程都会永远睡眠下去。

 

 

任何可能出错的地方终将出错。

 

临界区(critical section)

互斥(mutual exclusion)——某种可以阻止多个进程同时读写共享数据的途径。

 

避免竞争条件(race condition)的解决方案,需要满足四个条件

1、任何两个进程不能同时处于其临界区

2、不应对CPU的速度和数量做任何假设

3、临界区外运行的进程不得阻塞其他进程

4、不得使进程无限期等待进入临界区

 

 

几种实现互斥的方案

 

1、屏蔽中断

进程进入临界区之后立即屏蔽所有中断,并在就要离开之前再打开中断。屏蔽中断之后,时钟中断也被屏蔽。

弊端:

a、若一个进程屏蔽中断之后不再打开中断,整个系统可能会因此终止。

b、屏蔽中断仅对执行disable指令的那个cpu有效,其他CPU仍然可以继续执行。

 

2、锁变量

设置一把共享锁,设置其初始值为0.当一个进程想进入临界区,需要先测试这把锁。如果锁的值为0,则该进程将其设置为1并进入临界区。若锁的值已经为1,则等待。

弊端:

当一个进程测试锁的值为0时,此时发生一次时钟中断,CPU切换到另一个进程,另一个进程读取到锁的值为0,因此将锁的值改为1并进入临界区,再次发生时钟中断,CPU切换回来,刚刚读取锁的值为0的进程也将锁的值赋值为1并进入临界区。

 

3、严格轮换法

整型变量turn的初始值为0,用于记录哪一个进程进入临界区,并检查或更新共享内存。开始时,进程0检查turn,发现其值为0,则进入临界区,进程1发现turn的值为0,则进入忙等待(连续测试一个变量知道某个值出现为止,这种方法叫忙等待,用于忙等待的锁叫自旋锁)模式,等turn的值被进程0变成1之后,进程1才能进入临界区。

弊端:

在一个进程比另一个进程慢很多的情况下,该方法不是一个好方法。进程会被一个临界区之外的进程阻塞(情景:当进程0想进去临界区时,turn的值还是1,而此时进程1在忙于临界区之外的操作,进程0只能继续忙等待)

 

 

 

生产着消费者(三个经典同步问题那里还有更细致的描述)

 

模型概述:

两个进程共享一个公共固定大小的缓冲区,用一个变量count来跟踪缓冲区中的元素个数。其中一个是生产者,他将信息放入缓冲区,一个是消费者,他从缓冲区中取出信息。当消费者发现count的值为0时,就进行睡眠等待;当生产者发现cout的值等于缓冲区大小时,则进行睡眠等待。

问题:——可能会发生的竞争条件——由于count的访问未加限制

当消费者从缓冲区中取出一个元素之后,count的值变为0,此时此时调度程序决定暂停消费者并启动生产者。生产者想缓冲区中加入一个元素,此时生产者发现count的值变成了1,他推断由于各个count为0,所以消费者一定在睡眠,于是生产者迪奥哟经wakeup来唤醒消费者。

但是此时消费者并没有睡眠,因此wakeup信号丢失。当消费者运行时,他上一次读到的count值为0,因此进入睡眠。生产者迟早会填满缓冲区,然后睡眠。这样两个进程都会永远睡眠下去。

 

 

 

信号量

概念:

1、一个用于累计唤醒次数的整型变量

2、信号量(semaphore)的数据结构为一个值和一个指针,指针指向等待该信号量的下一个进程。信号量的值与相应资源的使用情况有关。

当它的值大于0时,表示当前可用资源的数量;

当它的值小于0时,其绝对值表示等待使用该资源的进程个数。

注意,信号量的值仅能由PV操作来改变。

 

信号量S>=0,S表示可用资源的数量。执行一次P操作意味着请求分配一个单位资源,因此S的值减1;

当S<0,表示已经没有可用资源,请求者必须等待别的进程释放该类资源,它才能运行下去。而执行一个V操作意味着释放一个单位资源,因此S的值加1;

若S<=0,表示有某些进程正在等待该资源,因此要唤醒一个等待状态的进程,使之运行下去。

 

用处

用于实现同步、用于实现互斥

 

 

 

互斥量——信号量的简化版本——没有信号量的计数能力

 

适用于

管理共享资源或一小段代码;实现用户空间线程包。

互斥量只有两种状态——解锁和加锁。

 

过程描述

当一个线程/进程需要访问临界区时,他调用mutex_lock。如果该互斥量当前是解锁的(即临界区可用),次调用成功,调用线程可以自由进入该临界区。

 

如果互斥量已经加锁,调用线程被阻塞,知道临界区中的线程完成并调用mutex_unlock。如果多个线程被阻塞在该互斥量上,将随机选择一个线程并允许他获得锁。

 

Thread_yied之所在用户空间中对线程调度的一个调用。因此mutex_lock和mutex_unlock都不需要任何内核调用。

 

 

 

Pthread

提供很多可以用来同步线程的函数。

基本机制

使用一个可以被锁定和解锁的互斥量保护每一个临界区。该互斥锁由程序员保证线程正确的使用它们。

条件变量——pthread的另一种同步机制

互斥量在允许火阻塞对临界区的访问上有用;

条件变量允许线程由于一些未达到的条件而阻塞。

 

生产者使用互斥量可以进行原子性检查。但是当发生缓冲区已满适,生产者需要一种方法阻塞自己并在以后唤醒,这便是条件变量该做的事了。

 

条件变量——互斥量模式

条件变量和互斥量经常一起使用。这种模式用于让一个线程锁住一个互斥量,然后当他不能获得他期待的结果时等待一个条件变量。最后另一个线程会向他发起信号,使他可以继续执行。

(注意:条件变量不像信号量,他不会存在内存中。如果将一个信号量传递给一个没有线程等待的条件变量,那么这个信号会丢失。程序员要小心使用避免丢失信号)

 

使用了互斥量和条件变量的,只有一个缓存的生产者消费者问题代码在阿里云里。去看吧,还是弄下来吧

  1 #include<stdio.h>
  2 #include<pthread.h>
  3 #define MAX 100000
  4 pthread_mutex_t the_mutex;
  5 pthread_cond_t condc, condp;
  6 int buffer=0;
  7 
  8 void * producer(void * ptr){
  9         for(int i=1 ; i<=MAX; i++){
 10                 pthread_mutex_lock(&the_mutex);
 11                 //自旋锁,循环等待,直到条件出现
 12                 while(buffer!=0) pthread_cond_wait(&condp, &the_mutex);
 13                 buffer=i;
 14                 printf("第%d个问题\n", i);
 15                 pthread_cond_signal(&condc);
 16                 pthread_mutex_unlock(&the_mutex);
 17         }
 18         //exit(0):正常运行程序并退出程序;
 19         pthread_exit(0);
 20 }
 21 
 22 void * consumer(void * ptr){
 23         for(int i=1; i<=MAX; i++){
 24                 pthread_mutex_lock(&the_mutex);
 25                 while(buffer==0) pthread_cond_wait(&condc, &the_mutex);
 26                 buffer=0;
 27                 printf("当然啦!\n");
 28                 //唤醒生产者
 29                 pthread_cond_signal(&condp);
 30                 pthread_mutex_unlock(&the_mutex);
 31         }
 32         pthread_exit(0);
 33 }
 34 
 35 int main(int argc, char *argv){
 36         pthread_t pro, con;
 37         pthread_mutex_init(&the_mutex, 0);
 38         pthread_cond_init(&condc, 0);
 39         pthread_cond_init(&condp, 0);
 40         pthread_create(&con, 0, consumer, 0);
 41         pthread_create(&pro, 0, producer, 0);
 42         pthread_join(pro, 0);
 43         pthread_join(con, 0);
 44         pthread_cond_destroy(&condc);
 45         pthread_cond_destroy(&condp);
 46         pthread_mutex_destroy(&the_mutex);
 47 }

 

 

 

原子操作

为了确保信号量可以正常工作,要采用一种不可分割的形式去实现它(原子操作:指一组相关联的操作要么都不间断的执行,要么都不执行)。保证一旦一个信号量操作开始,则在该操作完成或阻塞之前,其他进程均不允许访问该信号量。

 

 

 

PV操作

PV操作作为系统调用实现,执行以下动作时需要暂时屏蔽全部中断:测试信号量更新信号量以及在需要时使某个进程睡眠

PV操作由P操作原语和V操作原语组成(原语是不可中断的过程),对信号量进行操作,具体定义如下:

P(S):①将信号量S的值减1,即S=S-1;

             ②如果S<=0,则该进程继续执行;否则该进程置为等待状态,排入等待队列。

V(S):①将信号量S的值加1,即S=S+1;

             ②如果S>0,则该进程继续执行;否则释放队列中第一个等待信号量的进程。

p操作(wait):申请一个单位资源,进程进入

v操作(signal):释放一个单位资源,进程出来

 

 

 

 

三个经典的同步问题

 

生产着消费者

Mutex用于互斥,它用于保证任意时刻只有一个进城读写缓冲区和相关变量。

 

我们可把共享缓冲区中的n个缓冲块视为共享资源,生产者写人数据的缓冲块成为消费者可用资源,而消费者读出数据后的缓冲块成为生产者的可用资源。为此,可设置三个信号量:fullemptymutex

其中:full表示有数据的缓冲块数目,初值是0;

empty表示空的缓冲块数初值是n;

mutex用于访问缓冲区时的互斥,初值是1。

实际上,full和empty间存在如下关系:full + empty = N

 

注意:这里每个进程中各个P操作的次序是重要的(就是上面不能先申请mutex)。

各进程必须先检查自己对应的资源数在确信有可用资源后再申请对整个缓冲区的互斥操作;否则,先申请对整个缓冲区的互斥操后申请自己对应的缓冲块资源,就可能死锁。出现死锁的条件是,申请到对整个缓冲区的互斥操作后,才发现自己对应的缓冲块资源,这时已不可能放弃对整个缓冲区的占用。如果采用AND信号量集,相应的进入区和退出区都很简单。

 

 

读者写者

读者一写者问题(readers-writersproblem)是指多个进程对一个共享资源进行读写操作的问题。

假设“读者”进程可对共享资源进行读操作,“写者”进程可对共享资源进行写操作;任一时刻“写者”最多只允许一个,而“读者”则允许多个。即对共享资源的读写操作限制关系包括:“读—写,互斥、“写一写”互斥和“读—读”允许

我们可认为写者之间、写者与第一个读者之间要对共享资源进行互斥访问,而后续读者不需要互斥访问。

为此,可设置两个信号量WmutexRmutex和一个公共变量Rcount。其中:Wmutex表示“允许写”,初值是1;公共变量Rcount表示“正在读”的进程数,初值是0;Rmutex表示对Rcount的互斥操作,初值是1。

 

 

哲学家 

分析:

假如所有的哲学家都同时拿起左侧筷子,看到右侧筷子不可用,又都放下左侧筷子, 等一会儿,又同时拿起左侧筷子,如此这般,永远重复。对于这种情况,即所有的程序都在 无限期地运行,但是都无法取得任何进展,即出现饥饿,所有哲学家都吃不上饭。

描述一种没有人饿死(永远拿不到筷子)算法

A.原理:至多只允许四个哲学家同时进餐,

以下将room 作为信号量,只允 许4 个哲学家同时进入餐厅就餐,这样就能保证至少有一个哲学家可以就餐,而申请进入 餐厅的哲学家进入room 的等待队列,根据FIFO 的原则,总会进入到餐厅就餐,因此不会 出现饿死和死锁的现象。 

B.原理:仅当哲学家的左右两支筷子都可用时,才允许他拿起筷子进餐

利用信号量的保护机制实现。通过信号量mutexeat()之前的取左侧和右侧筷 子的操作进行保护,使之成为一个原子操作,这样可以防止死锁的出现

C. 原理:规定奇数号的哲学家先拿起他左边的筷子,然后再去拿他右边的筷子;而偶数号 的哲学家则相反.

按此规定,将是1,2号哲学家竞争1号筷子,3,4号哲学家竞争3号筷子.即 五个哲学家都竞争奇数号筷子,获得后,再去竞争偶数号筷子,最后总会有一个哲学家能获 得两支筷子而进餐。而申请不到的哲学家进入阻塞等待队列,根FIFO原则,则先申请的哲 学家会较先可以吃饭,因此不会出现饿死的哲学家。 

D.利用管程机制实现(最终该实现是失败的,见以下分析): 

原理:不是对每只筷子设置信号量,而是对每个哲学家设置信号量。test()函数有以下作 用: 

a. 如果当前处理的哲学家处于饥饿状态且两侧哲学家不在吃饭状态,则当前哲学家通过 test()函数试图进入吃饭状态。 

b. 如果通过test()进入吃饭状态不成功,那么当前哲学家就在该信号量阻塞等待,直到 

其他的哲学家进程通过test()将该哲学家的状态设置为EATING。 

c. 当一个哲学家进程调用put_forks()放下筷子的时候,会通过test()测试它的邻居, 如果邻居处于饥饿状态,且该邻居的邻居不在吃饭状态,则该邻居进入吃饭状态。 

由上所述,该算法不会出现死锁,因为一个哲学家只有在两个邻座都不在进餐时,才允 

许转换到进餐状态。 

该算法会出现某个哲学家适终无法吃饭的情况,即当该哲学家的左右两个哲学家交替 

处在吃饭的状态的时候,则该哲学家始终无法进入吃饭的状态,因此不满足题目的要求。 

但是该算法能够实现对于任意多位哲学家的情况都能获得最大的并行度,因此具有重要 的意义。

网友评论

登录后评论
0/500
评论
王小闹儿
+ 关注