• Dijkstra 和同事发明了信号量 可以使用信号量作为锁和条件变量

31.1 信号量的定义

  • 信号
  • 量是有一个整数值的对象

信号量的初始值能够决定其行为 所以首先要初始化信号量

  1. #include<semaphore.h>
  2. sem_t s;
  3. sem_init(s,0,1);

sem_init()通过第三个参数将信号量s的值初始化为1

sem_init()的第二个参数为0 ,表示信号量是在同一进程的多个线程共享的


信号量初始化之后 调用:sem_wait()和sem_post()与之交互

  1. int sem_wait(sem_t *s){
  2. decrement the value of semaphore s by one
  3. wait if value of semaphore s is negative
  4. }
  5. int sem_post(sem_t *s){
  6. increment the value of semaphore s by one
  7. if there are one or more threads waiting,wake one
  8. }

sem_wait()要么立刻返回(调用时,信号量的值≥1),要么会让调用线程挂起,直到之后的一个post操作。有可能多个调用线程都调用sem_wait(),因此都在队列中等待唤醒

sem_post()并没有等待某些条件满足。它直接增加信号量的值,如果有等待线成,唤醒其中一个。

当信号量值为负数的时候,这个值就是等待线程的个数

31.2 二值信号量(锁)

  • 信号量的第一种用法:作为锁

信号量的初值至关重要。一般为1。所以也叫二值信号量。

  1. sem_t m;
  2. sem_init(&m,0,1);
  3. sem_wait(&m);
  4. //临界区
  5. sem_post(&m);

31.3 信号量用作条件变量

  1. sem_t s;
  2. void *child(void *arg){
  3. printf("child\n");
  4. sem_post(&s);
  5. return NULL;
  6. }
  7. int main(int argc,char**argv){
  8. sem_init(&s,0,0); //关键
  9. printf("parent:begin\n");
  10. pthread_t c;
  11. pthread_create(c,NULL,child,NULL);
  12. sem_wait(&s);
  13. printf("parent:end\n");
  14. return 0;
  15. }

sem_post()会让信号量+1,即使没有wait()中的线程

31.4 生产者额/消费者(有界缓冲区)问题

  1. int buffer[MAX];
  2. int fill = 0;
  3. int use = 0;
  4. void put(int value){
  5. buffer[fill] = value;
  6. fill = (fill + 1) % MAX;
  7. }
  8. int get(){
  9. int tmp=buffer[use];
  10. use=(use + 1) % MAX;
  11. return tmp;
  12. }

put()和get()函数

  1. sem_t empty;
  2. sem_t full;
  3. void producer(void *arg){
  4. int i;
  5. for(int i=0;i<loops;++i){
  6. sem_wait(&empty);
  7. put(i);
  8. sem_post(&full);
  9. }
  10. }
  11. void consumer(void *arg){
  12. int i,tmp=0;
  13. while(tmp!=-1){
  14. sem_wait(&full);
  15. tmp=get();
  16. sem_post(&empty);
  17. printf("%d\n",tmp);
  18. }
  19. }
  20. int main(int argc,char**argv){
  21. //...
  22. sem_init(&empty,0,MAX);
  23. sem_init(&full,0,0);
  24. //...
  25. }
  • 当MAX等于1,即缓冲区大小为1的时候 该模型工作正常
  • 当MAX大于1时,如果有多个生产者消费者,会出现竞态条件:假如两个生产者同时调用put(),T2在T1将fill+1之前执行赋值操作,那么前一个put的数据将会被覆盖

解决方案:互斥 利用二值信号量来加锁

  1. sem_t empty;
  2. sem_t full;
  3. sem_t mutex;
  4. void producer(void *arg){
  5. int i;
  6. for(int i=0;i<loops;++i){
  7. sem_wait(&mutex);
  8. sem_wait(&empty);
  9. put(i);
  10. sem_post(&full);
  11. sem_post(&mutex);
  12. }
  13. }
  14. void consumer(void *arg){
  15. int i,tmp=0;
  16. while(tmp!=-1){
  17. sem_wait(&mutex);
  18. sem_wait(&full);
  19. tmp=get();
  20. sem_post(&empty);
  21. sem_post(&mutex);
  22. printf("%d\n",tmp);
  23. }
  24. }
  25. int main(int argc,char**argv){
  26. //...
  27. sem_init(&mutex,0,1);
  28. sem_init(&empty,0,MAX);
  29. sem_init(&full,0,0);
  30. //...
  31. }
  • 然而还是会有问题:死锁

当消费者持有mutex锁时,如果缓冲区为空,那么会被full信号量挂起。但是此时它仍持有mutex。此时对生产着的调用会让mutex出现死锁。


解决方案:缩小锁的作用域 把mutex放到full和empty之内即可

31.5 读者-写者锁

  • 读写锁:插入操作需要修改链表结构,而查找操作只需要读取该结构,只要没有插入操作,我们可以并发的执行多个查找操作
  • 当第一个读者获得读锁时,他也会获得写锁
  • 一单一个读者获得读锁,其他读者也可以获得这个锁。但是想要获得写锁的线程,就必须等到所有读者都结束。最后一个退出的写者,释放写锁。

31.6 哲学家就餐问题

哲学家围圆桌吃饭,只有同时获得左右手两个叉子才能吃饭

Dijkstra通过让最后一个人先尝试拿右手叉子,而其他人全都尝试拿左手叉子的逻辑实现

31.7 实现信号量

  • 用锁和条件变量实现信号量
  1. typedef struct _Zem_t{
  2. int value;
  3. pthread_cond_t cond;
  4. pthread_mutex_t lock;
  5. }Zem_t;
  6. void Zem_init(Zem_t *s,int value){
  7. s->value=value;
  8. Cond_init(&s->cond);
  9. Mutex_init(&s->lock);
  10. }
  11. void Zem_wait(Zem_t *s){
  12. Mutex_lock(&s->lock);
  13. while(s->val<=0)
  14. Cond_wait(&s->cond,&s->lock);
  15. s->value--;
  16. Mutex_unlock();
  17. }
  18. void Zem_post(Zem_t *s){
  19. Mutex_lock(&s->lock);
  20. s->value++;
  21. Cond_signal(&s->cond);
  22. Mutex_unlock(&s->lock);
  23. }