单缓冲区
问题描述:
共享缓冲区为环形,为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防止交叉操作对链表指针的破坏。