在上一节已经讲解了信号量的基本原理,并用信号量实现了一个简单的锁变量机制,本节将使用信号量进行一个更为复杂系统的实现,对信号量的理解要求更高。
但首先要先明确,再怎么复杂它都是有核心概念的,就是:
当对某个信号量调用sem_wait(sem)时,只要sem为正数,就能继续进行运行,同时sem减一,否则阻塞在该瞬间。
而调用sem_post(sem)时,sem首先会加一,并能放行一个被阻塞的线程。
而其它的约束都是没有的,比如sem的初始值应该是多少啊,什么时候进行wp操作啊,wp需不需要成对出现啊。这些都是根据需要设定即可。
复杂的生产者-消费者模型:
我们设计这样的模型:有a个生产者、b个消费者,
生产者要花费一定时间生产一个产品,用一个值value表示,然后放进一个队列中。
消费者从队列中取出一个产品,花费一定时间消耗掉它。
这个队列就是该模型中的有界缓冲区。
接下来列出我们要在哪里、设计一个怎样的信号量才能实现所有共享数据的正确
产品值value,任意时刻只能有一个生产者访问到value并将至自增1(表示产生好一个产品,下一次生产value+1的产品)
队列头h,任意时刻只能有一个消费者访问h,取出队列中的头元素并将h后移
队列尾t,任意时刻只能有一个生产者访问t,放入队列中一个尾元素(生产者产出的产品value)
这里我们使用一个循环队列,那么问题来了,假设生产者运行生产飞快,消费者消费很慢,显然队列满的时候不可以放入。
同理消费者消费很快时,队列空时不可以继续消费。
因此这里要需要特殊信号量的设计:
canRead,控制型信号量,当队列非空时,canRead为一个正数,消费者要进行消费前,要先调用sem_wait(canRead),在canRead为非正数时说明此时队列中没有值可以取出消费,从而实现阻塞在这里,等待队列填充
canInput,控制型信号量,同样道理,保证队列满时,生产者会被阻塞起来,从而不往里放元素。
两者的初始值也需要进行仔细设计:
canRead,在一开始显然队列为空,所有的消费者都无法进行消费,可见一开始就得都阻塞起来,canRead都为0
canInput,在一开始显然能放入等于队列大小的元素,因此canInput为队列大小。
调用sempost的地方也很有意思,生产者成功地完整地放进去一个元素后,才可以让消费者读,所以sempost(canRead)在生产者放进去后再调用
canInput同样,是在消费者取出来后,才可以调用其sempost
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
#include<semaphore.h>
#include<sys/select.h>
#include<sys/time.h>
#define F(i,a,b) for(int i=a;i<=b;i++)
#define MAXN 2
sem_t semValue,semTail,semHead,canInput,canRead;
int que[MAXN];
int h=0,t=0,value=1;
void msleep(int ms){
struct timeval tval;
tval.tv_sec=ms/1000;
tval.tv_usec=(ms*1000)%1000000;
select(0,NULL,NULL,NULL,&tval);
}
void provideData(){
while(1){
sem_wait(&semValue);
int providedNum = value++;
sem_post(&semValue);
printf("now providing value = %d\n", providedNum);
msleep(2000);
sem_wait(&canInput);
sem_wait(&semTail);
que[t] = providedNum;
t = (t + 1) % MAXN;
sem_post(&semTail);
sem_post(&canRead);
printf("now provided and inputed value = %d\n", providedNum);
}
}
void consumerData(){
while(1){
sem_wait(&canRead);
sem_wait(&semHead);
int consumedNum = que[h];
h = (h + 1) % MAXN;
sem_post(&semHead);
sem_post(&canInput);
printf("now consume value = %d\n", consumedNum);
msleep(3000);
printf("now has consumd value = %d\n", consumedNum);
}
}
int main(){
int a,b;
printf("input the number of providers and consumers:\n");
scanf("%d %d",&a,&b);
pthread_t* provider, *consumer;
provider = malloc(a * sizeof(pthread_t));
consumer = malloc(b * sizeof(pthread_t));
sem_init(&semValue, 0, 1);
sem_init(&semHead, 0, 1);
sem_init(&semTail, 0, 1);
sem_init(&canRead, 0, 0);
sem_init(&canInput, 0, MAXN);
F(i,0,a-1){
pthread_create(provider + i, NULL, (void *)provideData, NULL);
}
F(i,0,b-1){
pthread_create(consumer + i, NULL, (void *)consumerData, NULL);
}
F(i,0,a-1){
pthread_join(provider[i], NULL);
}
F(i,0,b-1){
pthread_join(consumer[i], NULL);
}
sem_destroy(&semValue);
sem_destroy(&semHead);
sem_destroy(&semTail);
sem_destroy(&canRead);
sem_destroy(&canInput);
}
为了方便展示,这里设计有界缓冲区只有2个元素的大小
[root@localhost multithread]# ./main.out
input the number of providers and consumers:
3 3
now providing value = 1
now providing value = 2
now providing value = 3
now provided and inputed value = 3
now providing value = 4
now consume value = 3
now provided and inputed value = 2
now providing value = 5
now provided and inputed value = 1
now providing value = 6
now consume value = 1
now consume value = 2
now provided and inputed value = 4
now providing value = 7
now provided and inputed value = 5
now providing value = 8
now has consumd value = 3
now consume value = 4
now provided and inputed value = 6
now providing value = 9
这里我们安排3个生产者,3个消费者,
从输出中看到,先是3个生产者进行工作,然后放进去一个3号后,开始生产4,3随机立马被拿出去消费,2、1号顺势完成放进队列后,开始生产5、6,后1、2号再被消费。
这里value的自增也是正确的、生产、消费顺序也是合法的。
大家自己试试,把生产、消费的时间拉长动态观察分析分析,应该能更好地理解。
这里其实还有能继续要深入理解的地方,比如一开始我们可能所有的consumer都被canRead阻塞了,那么其实根本就不存在canRead大与0的时刻,但是在生产者生产后调用sempost(canRead),还能放行一个消费者立刻去读取,所以说canRead这个信号量在为负数的时候其绝对值才表示“阻塞”在其上的线程数。
因此前文的理解也还有发展空间,这里就是:阻塞在信号量上的线程会在sempost时放行一个,核心是维护信号量在为负数的时候其绝对值表示“阻塞”在其上的线程数