目录

#include <semaphore.h>
sem_t s;
sem_init(&s, 0, 1);
// param2:0:信号量在同进程的多线程中共享
// param3:信号量的值
int sem_wait(sem_t* s) {
  // s--;
  s>=0 -> 立即返回
  s<0 线程挂起,直到之后的post操作
}

int sem_post(sem_t* s) {
  // s++;
  直接增加信号量的值,如果有等待的线程,唤醒其中一个。
}
// 当信号量 < 0 时,s的val就是等待线程的个数。

二值信号量(锁)

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

直接把临界区用一对sem_wait() / sem_post()包括起来。

0值信号量(条件变量)

信号量的第一种用法:作为条件变量来使用

父线程wait等待子线程

子线程done后调用post进行signal

sem_t* s;
void* child(void* arg) {
  printf("child\n");
  sem_post(&s); // signal : child done
  return NULL;
}

int main() {
  sem_init(&s, 0, 0); // 初始化值为0
  printf("parent:begin\n");
  pthread_t p;
  Pthread_create(p, NULL, child, NULL);
  sem_wait(&s); // wait : wait for child done
  printf("parent:end\n");
  return 0;
}

综合使用条件变量和

nt buffer[MAXN];
int fill_ptr = 0;
int use_ptr = 0;
int count = 0;

void put(int value) {
    buffer[fill_ptr] = value;
    fill_ptr = (fill_ptr + 1) % MAXN;
    count++;
}

int get() {
    int tmp = buffer[use_ptr];
    use_ptr = (use_ptr + 1) % MAXN;
    count--;
    return tmp;
}

sem_t empty;
sem_t full;
sem_t mutex;

void* producer(void* arg) {
  for (int i = 0; i < loops; i++) {
    sem_wait(&empty); // 等到空的时候进行生产
    sem_wait(&mutex); // 为了保证生产者互斥插入,需要加锁
    put(i);
    sem_post(&mutex); // 解锁
    sem_post(&full);  // 生产好了,提醒消费者进行消费
  }
}

void* consumer(void* arg) {
  for (int i = 0; i < loops; i++) {
    sem_wait(&full); // 等到生产好了进行消费
    sem_wait(&mutex); // 为了保证消费者互斥插入,需要加锁
    put(i);
    sem_post(&mutex); // 解锁
    sem_post(&empty);  // 消费好了,提醒生产者进行生产
  }
}

举例:

在这里出现了死锁,因为sem_wait在让出CPU时并没有释放锁,而在https://www.wolai.com/dNjJbFaR3fYqd6CXP27HAe中,我们使用的Pthread_cond_wait(&empty, &mutex);会对锁进行释放,从而避免死锁。这两个API的区别注意一下。

读写锁

访问同一个数据结构可能需要不同类型的锁。读写锁就是一种。

下面是不公平的读写锁:写锁只能在最后读者释放之后才能被写者获取。

rwlock_acquire_writelock() 获得写锁
rwlock_release_writelock() 释放写锁

typedef struct _rwlock_t {
    sem_t lock;         // 二值信号量作为basic lock
    sem_t writelock;    // 用于一个写者 or 多个读者
    int readers;        // 在临界区读取的读者数
} rwlock_t;

void rwlock_init(rwlock_t* rw) {
    rw->readers = 0;
    sem_init(&rw->lock, 0, 1);
    sem_init(&rw->writelock, 0, 1);
} 

void rwlock_acquire_readlock(rwlock_t* rw) {
    sem_wait(&rw->lock);
    rw->readers++;
    if (rw->readers == 1) {
        sem_wait(&rw->writelock);   // 第一个读者需要持有写锁
    }
    sem_post(&rw->lock);
}

void rwlock_release_readlock(rwlock_t* rw) {
    sem_wait(&rw->lock);
    rw->readers--;
    if (rw->readers == 0) {
        sem_post(&rw->writelock);   // 最后一个读者释放写锁
    }
    sem_post(&rw->lock);
}

void rwlock_acquire_writelock(rwlock_t* rw) {
    sem_wait(&rw->writelock);
}

void rwlock_release_writelock(rwlock_t* rw) {
    sem_post(&rw->writelock);
}

一旦一个读者获得了读锁,其他的读者也可以获取这个读锁。

但是,想要获取写锁的线程,就必须等到所有的读者都结束。

最后一个退出的写者在“writelock”信号量上调用sem_post(),从而让等待的写者能够获取该锁。

读者很容易饿死写者,我们应该思考有写者等待时,如何能够避免更多的读者进入并持有锁。

这里TODO一下,以后看到有好的代码写过来。

假定有5位“哲学家”围着一个圆桌。每两位哲学家之间有一把餐叉(一共5把)。哲学家有时要思考一会,不需要餐叉;有时又要就餐。而一位哲学家只有同时拿到了左手边和右手边的两把餐叉,才能吃到东西。

每个哲学家(线程)的自我循环:

while (1) {
  think();
  getforks();
  eat();
  putforks();
}

辅助函数:

int left(int p) { // 使用左边的叉子
  return p;
}
int right(int p) { // 使用右边的叉子
  return (p + 1) % 5;
}

sem_t forks[5];
void getforks() {
  sem_wait(forks[left(p)]);
  sem_wait(forks[right(p)]);
}

依次获取每把餐叉的锁——先是左手边的,然后是右手边的。结束就餐时,释放掉锁。假设每个哲学家都拿到了左手边的餐叉,他们每个都会阻塞住,并且一直等待另一个餐叉。

解决上述问题最简单的方法,就是修改某个或者某些哲学家的取餐叉顺序。

假定哲学家4(编写最大的一个)取餐叉的顺序不同。

因为最后一个哲学家会尝试先拿右手边的餐叉,然后拿左手边,所以不会出现每个哲学家都拿着一个餐叉,卡住等待另一个的情况,等待循环被打破了。

使用锁+条件变量实现信号量

typedef struct _Zem_t {
    int value;
    pthread_cond_t cond;
    pthread_mutex_t lock;
} Zem_t;

// only one thread can call it
void Zem_init(Zem_t* s, int value) {
    s->value = value;
}

void Zem_wait(Zem_t* s) {
   Pthread_mutex_lock(&s->lock);
   while (s->value <= 0) {  // 当小于0 阻塞 并在睡眠前将锁释放,这样下面的unlock就不会出错
        Pthread_cond_wait(&s->cond, &s->lock);
   }
   s->value--;
   Pthread_mutex_unlock(&s->lock);
}

void Zem_post(Zem_t* s) {
   Pthread_mutex_lock(&s->lock);
   s->value++;
   Pthread_cond_signal(&s->cond);
   Pthread_mutex_unlock(&s->lock);
}

扩展

关于信号量的之前也有部分笔记:

linux进程间通信快速入门【三】:信号量(XSI、POSIX以及PV原语)_拾牙慧者的博客-CSDN博客_linux xsi 在前两篇文章中我们使用的racingdemo都没有对临界区代码进行加锁,这里我们介绍以下信号量的使用。Linux环境下主要实现的信号量有两种。根据标准的不同,它们跟共享内存类似,一套XSI的信号量,一套POSIX的信号量。下面我们分别使用它们实现一套类似文件锁的方法,来简单看看它们的使用。参考https://zorrozou.github.io/docs/books/linuxde-jin-che https://hanhandi.blog.csdn.net/article/details/123576424