对于用Posix信号量解决读者-写者问题的小研究


单缓冲区



问题描述:

        共享缓冲区为环形,为NBUFF个元素的int数组。若干个生产者,若干个消费者在进行读写操作。



一个读者,一个写者:

需要三种信号量:
        mutex二值信号量,保护读和写操作。可以用互斥锁代替。
        nempty信号量,初始化数量为空槽数量(NBUFF)。
        nstored信号量,初始化数量为0。

主函数:
        输入:nitems
        初始化三个信号量。create两个线程,然后join它们。

produce:
for( i = 0 ; i < nitems ; ++i)
{
        等待空信号量;
        等待mutex;
        生产;
        释放mutex;
        等待空信号量;
}

consume:
for( i = 0 ; i< nitems;++i)
{
        等待nstored信号量;
        等待mutex;
        消费;
        释放mutex;
        释放nstored信号量;
}

代码:


#include "../common.h" 

#define NBUFF 10 
#define SEM_MUTEX "mutex" 
#define SEM_NEMPTY "nempty" 
#define SEM_NSTORED "nstored" 

#define FILE_MODE (S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH) 

struct { 
  int buff[NBUFF]; 
  sem_t *mutex, *nempty, *nstored; 
} shared; 

int nitems; 

void* produce(void*); 
void* consume(void*); 

int main(int argc, char** argv) 
{ 
  
  pthread_t produce_tid, consume_tid; 

  if( argc!=2 ) 
    err_exit("usage: prodcons1 <nitems>"); 

  nitems = atoi(argv[1]); 

  sem_unlink(SEM_MUTEX); 
  sem_unlink(SEM_NEMPTY); 
  sem_unlink(SEM_NSTORED); 

  if( (shared.mutex = sem_open(SEM_MUTEX, O_CREAT|O_RDWR, FILE_MODE, 1)) == SEM_FAILED) 
    err_exit("sem_open"); 
  if( (shared.nempty = sem_open(SEM_NEMPTY, O_CREAT|O_RDWR, FILE_MODE, NBUFF)) == SEM_FAILED) 
    err_exit("sem_open"); 
  if( (shared.nstored = sem_open(SEM_NSTORED, O_CREAT|O_RDWR, FILE_MODE, 0)) == SEM_FAILED) 
    err_exit("sem_open"); 

  pthread_create(&produce_tid, NULL, produce, NULL); 
  
  pthread_create(&consume_tid, NULL, consume, NULL); 

  pthread_join(produce_tid, NULL); 
  pthread_join(consume_tid, NULL); 
  
  sem_unlink(SEM_MUTEX); 
  sem_unlink(SEM_NEMPTY); 
  sem_unlink(SEM_NSTORED); 
  exit(0); 
} 


void* produce( void* val) 
{ 
  int i; 

  for( i = 0 ; i < nitems ; ++i) 
    { 
      sem_wait(shared.nempty); 
      sem_wait(shared.mutex); 
      printf("put %4d\n", i); 
      shared.buff[i%NBUFF] = i; 
      sem_post(shared.mutex); 
      sem_post(shared.nstored); 
    } 
  return NULL; 
} 

void* consume(void* val) 
{ 
  int i; 

  for( i = 0 ; i < nitems ; ++i) 
    { 
      sem_wait(shared.nstored); 
      printf("get nstored\n"); 
      sem_wait(shared.mutex); 
      if( shared.buff[i%NBUFF] != i) 
	fprintf(stderr, "buff[%d] = %d\n", i, shared.buff[i%NBUFF]); 
      sem_post(shared.mutex); 
      sem_post(shared.nempty); 
    } 

  return NULL; 
}

分析:

        在这个例子中,mutex不是必须的,因为消费和生产的过程完全互不影响。但,一般情况下是需要的,建议一直加上。
        注意加锁顺序,否则可能死锁。
        这个代码里使用的是有名信号量,实际上使用基于内存的信号量还更简单点。


多个生产者,一个消费者


        所需要的信号量不变,mutex,nstored和nempty足以应付多个生产者的情况。
        需要nput和nval来标明下一个生产者要填入的位置和数据。

主函数:
        输入nitems、nthreads;
        生成nthreads个生产者线程;
        生成一个消费者线程;
        等待他们结束;
        输出每个线程的统计数量。

produce:
while(1)
{
        等待nempty和mutex;
        检查是否已经生产nitems个项;
        没有的话,挂出mutex和nempty。退出。
        生产;
        挂出nstored和mutex
        增加自己的生产数量统计;
}

consume:
for(i=0;i<nitems;++i)
{
        等待nstored和mutex;
        检查是否正确;
        挂出nempty和mutex;
}

代码(使用了基于内存的信号量)


#include "../common.h"

#define NBUFF 10
#define MAXNTHREADS 100

struct {
  int buff[NBUFF];
  int nput;
  int nval;
  sem_t mutex, nempty, nstored;
} shared;

int nitems;

void* produce(void*);
void* consume(void*);

int main(int argc, char** argv)
{
  
  pthread_t produce_tid[MAXNTHREADS], consume_tid;
  int nthreads, i;
  int count[MAXNTHREADS];

  if( argc!=3 )
    err_exit("usage: prodcons1 <nitems> <threads>");

  nitems = atoi(argv[1]);
  nthreads = (atoi(argv[2]) > MAXNTHREADS)? MAXNTHREADS : atoi(argv[2]);

  if( (sem_init(&shared.mutex, 0, 1)) == -1)
    err_exit("sem_init");
  if( (sem_init(&shared.nempty, 0, NBUFF)) == -1)
    err_exit("sem_init");
  if( (sem_init(&shared.nstored, 0, 0)) == -1)
    err_exit("sem_init");

  for( i = 0 ; i < nthreads ; ++i)
    {
      count[i] = 0;      
      pthread_create(&produce_tid[i], NULL, produce, &count[i]);
    }

  pthread_create(&consume_tid, NULL, consume, NULL);
  pthread_join(consume_tid, NULL);

  for( i = 0 ; i < nthreads ; ++i)
    {
      pthread_join(produce_tid[i], NULL);
      printf("count[%d] = %d\n", i, count[i]);
    }

  exit(0);
}


void* produce( void* val)
{

  while(1)
    {
      sem_wait(&shared.nempty);
      sem_wait(&shared.mutex);
      if(shared.nput >= nitems)
	{
	  sem_post(&shared.mutex);
	  sem_post(&shared.nempty);
	  return NULL;
	}
      shared.buff[(shared.nput++)%NBUFF] = shared.nval++;
      sem_post(&shared.mutex);
      sem_post(&shared.nstored);
      ++*((int*)val);
    }
  return NULL;
}

void* consume(void* val)
{
  int i;

  for( i = 0 ; i < nitems ; ++i)
    {
      sem_wait(&shared.nstored);
      sem_wait(&shared.mutex);
      if( shared.buff[i%NBUFF] != i)
	fprintf(stderr, "buff[%d] = %d\n", i, shared.buff[i%NBUFF]);
      sem_post(&shared.mutex);
      sem_post(&shared.nempty);
    }

  return NULL;
}


分析:

        能同时获得nempty的生产者有多个,但能获得mutex的只有一个。
        生产者在正常生产结束后和已达到要求退出时,都要挂出mutex,但另一个挂出的分别为nstored和nempty。


多个生产者,多个消费者


当消费者变为多个时,需要类似于nput和nval的变量nget和ngetval来同步消费者。

主函数:
        输入nitems,nthreads,nconsumers;
        产生nthreads个生产者线程,nconsumers个消费者线程;
        等待他们结束;
        分别输出统计数量。

produce:
while(1)
{
        等待nempty和mutex;
        检查是否已经生产nitems个项;
        是的话,挂出mutex和nempty以及NSTORED!。退出。
        生产;
        挂出nstored和mutex
        增加自己的生产数量统计;
}

consume:
for(i=0;i<nitems;++i)
{
        等待nstored和mutex;
        检查是否已经检查了nitems个项;
        是的话,挂出mutex和nstored。退出。
        检查是否正确;
        挂出nempty和mutex;
        增加自己的消费数量统计;
}

代码:

#include "../common.h"

#define NBUFF 10
#define MAXNTHREADS 100

struct {
  int buff[NBUFF];
  int nput;
  int nval;
  int nget;
  int ngetval;
  sem_t mutex, nempty, nstored;
} shared;

int nitems;

void* produce(void*);
void* consume(void*);

int main(int argc, char** argv)
{
  
  pthread_t produce_tid[MAXNTHREADS], consume_tid[MAXNTHREADS];
  int nthreads, nconsumers, i;
  int count[MAXNTHREADS], count2[MAXNTHREADS];

  if( argc!=4 )
    err_exit("usage: prodcons1 <nitems> <threads> <nconsumers>");

  nitems = atoi(argv[1]);
  nthreads = (atoi(argv[2]) > MAXNTHREADS)? MAXNTHREADS : atoi(argv[2]);
  nconsumers = (atoi(argv[3]) > MAXNTHREADS)? MAXNTHREADS : atoi(argv[3]);

  if( (sem_init(&shared.mutex, 0, 1)) == -1)
    err_exit("sem_init");
  if( (sem_init(&shared.nempty, 0, NBUFF)) == -1)
    err_exit("sem_init");
  if( (sem_init(&shared.nstored, 0, 0)) == -1)
    err_exit("sem_init");

  for( i = 0 ; i < nthreads ; ++i)
    {
      count[i] = 0;      
      pthread_create(&produce_tid[i], NULL, produce, &count[i]);
    }

  for( i = 0 ; i < nconsumers ; ++i)
    {
      count2[i] = 0;
      pthread_create(&consume_tid[i], NULL, consume, &count2[i]);
    }

  for( i = 0 ; i < nconsumers ; ++i)
    {
      pthread_join(consume_tid[i], NULL);
      printf("count2[%d] = %d\n",i, count2[i]);
    }

  for( i = 0 ; i < nthreads ; ++i)
    {
      pthread_join(produce_tid[i], NULL);
      printf("count[%d] = %d\n", i, count[i]);
    }

  exit(0);
}


void* produce( void* val)
{

  while(1)
    {
      sem_wait(&shared.nempty);
      sem_wait(&shared.mutex);
      if(shared.nput >= nitems)
	{
	  sem_post(&shared.nstored);
	  sem_post(&shared.mutex);
	  sem_post(&shared.nempty);
	  return NULL;
	}
      shared.buff[(shared.nput++)%NBUFF] = shared.nval++;
      sem_post(&shared.mutex);
      sem_post(&shared.nstored);
      ++*((int*)val);
    }
  return NULL;
}

void* consume(void* val)
{

  while(1)
    {
      sem_wait(&shared.nstored);
      sem_wait(&shared.mutex);
      if( shared.nget >= nitems)
	{
	  sem_post(&shared.mutex);
	  sem_post(&shared.nstored);
	  return NULL;
	}
      if( shared.buff[(shared.nget++)%NBUFF] != shared.ngetval++)
	fprintf(stderr, "buff[%d] = %d\n", shared.nget-1, shared.buff[(shared.nget-1)%NBUFF]);
      sem_post(&shared.mutex);
      sem_post(&shared.nempty);

      ++*((int*)val);
    }

  return NULL;
}

分析:

        与之前的不同,produce函数里的if语句内,多了一行sem_post(&shared.nstored)。看上去,这个不应该出现。这行代码的作用是为了使消费者结束。设想,最后的消费者读完最后一个item后,循环回去又会等待nstored信号量,可这时生产者已经生产完毕,不会再多挂出一个nstored信号量。于是,生产者里退出时的这个sem_post(&shared.nstored),为了多给出至少一个信号量使消费者退出(消费者拿到后,发现都读完了,进入if又挂出了nstored。实际上,这个nstored多一个就可以)。

        一个基本原则,生产消费问题的正常终止状态和开始状态的信号量是一样的。




多缓冲区



问题描述:

        在文件复制或显示的程序中,常见的这种读写问题: while( fgets()) fputs();
        当使用一个缓冲区时,即使读写分开线程,由于缓冲区共享也不会加快。
        使用NBUFF个缓冲区,每个缓冲区NBUFFSIZE大小。一个写者,一个读者,来回向NBUFF个缓冲区上放数据取数据。


一个读者,一个写者


需要的信号量:

        nempty信号量,初始化数量为缓冲区数量NBUFF。
        nstored信号量,初始化数量为0,代表开始时没有缓冲区有数据。
        mutex二值信号量,在这个例子中,由于只有一个读一个写,而且读和写的操作相互独立,mutex不需要。如果,涉及到链表的增加删除,就需要mutex在读和写进行链表修改时同步。

main:
        打开要读入的文件;
        初始化信号量;
        生成读和写线程,并等待其结束。

produce:
while(1)
{
        等待nempty信号量;
        读取文件,填充下一个缓冲区;
        如果读到文件末尾,那么挂出一个额外的nstored信号量(原因同多读多写的例子),退出;
        挂出nstored信号量
}

consume:
while(1)
{
        等待nstored信号量;
        查看下一个缓冲区;
        如果缓冲区的字节数为0,退出;
        将缓冲区的数据写到要输出的fd;
        挂出nempty信号量;
}

代码:
#include "../common.h"

#define NBUFF 8
#define NBUFFSIZE 4096

struct {
  struct {
    char data[NBUFFSIZE];
    int  n;
  } buff[NBUFF];
  sem_t nempty, nstored;
} shared;

int fd;

void* produce(void*);
void* consume(void*);

int main( int argc, char** argv)
{
  pthread_t tid_produce, tid_consume;

  if(argc != 2)
    err_exit("usage: mycat2 <filename>");

  if( (fd=open(argv[1], O_RDWR)) == -1)
    err_exit("open");

  if( sem_init(&shared.nempty, 0, NBUFF) == -1)
    err_exit("sem_init");
  if( sem_init(&shared.nstored, 0, 0) == -1)
    err_exit("sem_init");

  pthread_create(&tid_produce, NULL, produce, NULL);
  pthread_create(&tid_consume, NULL, consume, NULL);

  //printf("create threads OK!\n");

  pthread_join(tid_consume, NULL);
  pthread_join(tid_produce, NULL);
  
  exit(0);
}

void* produce (void* val)
{
  int i=0;

  while(1)
    {
      sem_wait(&shared.nempty);
      //printf("get nempty\n");
      shared.buff[i].n = read(fd, shared.buff[i].data, NBUFFSIZE);
      if(shared.buff[i].n == 0 )
	{
	  sem_post(&shared.nstored);
	  return NULL;
	}
      i = (i+1) % NBUFF;
      
      sem_post(&shared.nstored);
    }
  return NULL;
}

void* consume(void* val)
{
  int i = 0 ; 
  
  while(1)
    {
      sem_wait(&shared.nstored);
      
      if(shared.buff[i].n == 0 || shared.buff[i].n == -1)
	return NULL;

      write(STDOUT_FILENO, shared.buff[i].data, shared.buff[i].n);
      i = (i+1)%NBUFF;

      sem_post(&shared.nempty);
    }
  return NULL;
}

分析:
        如前所述,单纯的将读和写分开两个线程总时间并不能获得任何速度上的优势。
        这个例子没有使用mutex,因为读和写唯一而且互不相干。如果使用链表方式管理缓冲区,读的时候新分配一个缓冲区并添加到链表,写的时候去掉一个链表,这样的话我们就需要加入mutex防止交叉操作对链表指针的破坏。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值