操作系统笔记(13)-信号量

简介

  • 信号量是另一种提供线程同步的方式,可以使用信号量作为锁和条件变量

POSIX 信号量 API

#include <semaphore.h>

// 初始化信号量
// 1. sem_t 变量
// 2. pshared 为0表示这个信号量是当前进程的局部信号量, 否则该信号量可以在多个进程之间共享
// 3. value 指定信号量初始值
int sem_init(sem_t* sem, int pshared, unsigned int value);

// 销毁信号量
int sem_destroy(sem_t* sem);

// 以原子操作的方式将信号量的值减一。 减完后信号量的值若小于0,就等待至信号量大于等于0
int sem_wait(sem_t* sem);

// 始终立即返回, 当信号量为0时返回-1并设置errno
int sem_trywait(sem_t* sem);

// 原子地将信号量加一
int sem_post(sem_t* sem);

信号量充当互斥锁

  • 将信号量的初始值设置为 1 即可
#include <stdio.h>
#include <semaphore.h>
#include <pthread.h>

int counter = 0;

sem_t mutex;

void* add(void *arg) {
    int loops = *(int*)arg;
    for (int i = 0; i < loops; i++) {
        sem_wait(&mutex);
        ++counter;
        sem_post(&mutex);
    }
}

int main(int argc, char* argv[]) {
    int cnt;
    printf("Please input count : ");
    scanf("%d", &cnt);
    sem_init(&mutex, 0, 1);
    pthread_t t[2];
    pthread_create(&t[0], NULL, add, (void*)&cnt);
    pthread_create(&t[1], NULL, add, (void*)&cnt);

    pthread_join(t[0], NULL);
    pthread_join(t[1], NULL);

    printf("counter : %d\n", counter);

    sem_destroy(&mutex);
    return 0;
}

信号量充当条件变量

  • 将信号量的初始值设置为 0 即可, 满足条件后 post
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>

sem_t cond;

void* child(void *arg) {
    printf("child\n");
    // do something...
    sleep(1);
    sem_post(&cond);    // 相当于唤醒
}

int main(int argc, char* argv[]) {
    printf("parent : begin\n");
    sem_init(&cond, 0, 0);
    pthread_t p;
    pthread_create(&p, NULL, child, NULL);

    sem_wait(&cond);    // 主线程等待子线程完成任务
    printf("parent : end\n");
    sem_destroy(&cond);
    return 0;
}

利用信号量解决生产者/消费者问题

#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>

#define MAX 20

int buffer[MAX];
int fill_ptr;
int use_ptr;

sem_t full, empty;
sem_t mutex;

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

int get() {
    int value = buffer[use_ptr];
    use_ptr = (use_ptr + 1) % MAX;
    return value;
}

void* producer(void *arg) {
    int loops = *(int*)arg;
    for (int i = 0; i < loops; i++) {
        sem_wait(&empty);   // 如果有空位
        sem_wait(&mutex);
        put(i);
        printf("put : %d\n", i);
        sem_post(&mutex);
        sem_post(&full);    // 增加一个非空位
    }
}

void* consumer(void *arg) {
    int loops = *(int*)arg;
    for (int i = 0; i < loops; i++) {
        sem_wait(&full);    // 如果有非空位
        sem_wait(&mutex);
        int value = get();
        printf("get : %d\n", value);
        sem_post(&mutex);
        sem_post(&empty);   // 增加一个空位
    }
}

利用信号量实现读写锁

  • 读写锁允许一个线程获得 写锁 , 多个线程获得 读锁,这样可以让多个线程同时进行 操作

  • 读写锁实现如下:

#ifndef _READER_WRITER_LOCK_H_
#define _READER_WRITER_LOCK_H_

#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>

typedef struct rwlock_t {
    sem_t lock;
    sem_t writelock;
    int readers;
} rwlock_t;

// 初始化读写锁
void rwlock_init(rwlock_t *rw) {
    sem_init(&rw->lock, 0, 1);
    sem_init(&rw->writelock, 0, 1);
    rw->readers = 0;
}

// 获得读锁
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);
}

#endif

测试

#include "reader-writer-lock.h"
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int loops;
int value = 0;

rwlock_t lock;

void *reader(void *arg) {
    int i;
    for (i = 0; i < loops; i++) {
	    rwlock_acquire_readlock(&lock);
	    printf("read %d\n", value);
	    rwlock_release_readlock(&lock);
    }
    return NULL;
}

void *writer(void *arg) {
    int i;
    for (i = 0; i < loops; i++) {
	    rwlock_acquire_writelock(&lock);
	    value++;
	    printf("write %d\n", value);
	    rwlock_release_writelock(&lock);
    }
    return NULL;
}

int main(int argc, char *argv[]) {
    assert(argc == 4);
    int num_readers = atoi(argv[1]);
    int num_writers = atoi(argv[2]);
    loops = atoi(argv[3]);

    pthread_t pr[num_readers], pw[num_writers];

    rwlock_init(&lock);

    printf("begin\n");

    int i;
    for (i = 0; i < num_readers; i++)
	    pthread_create(&pr[i], NULL, reader, NULL);
    for (i = 0; i < num_writers; i++)
	    pthread_create(&pw[i], NULL, writer, NULL);

    for (i = 0; i < num_readers; i++)
	    pthread_join(pr[i], NULL);
    for (i = 0; i < num_writers; i++)
	    pthread_join(pw[i], NULL);

    printf("end: value %d\n", value);

    return 0;
}

缺点 :一旦一个读者获得了 读锁 ,其他的读者也可以获取这个读锁 。但是,想要获取 写锁 的线程,就必须等到所有的读者都结束,因此 读者很容易饿死写者 , 并且和其他一些简单快速的锁相比,读写锁在性能方面没有优势

哲学家就餐问题

哲学家进餐

  • 这是一个著名的并发问题,问题的基本情况是:假定有 5 位“哲学家”围着一个圆桌,每两位哲学家之间会有一把叉子(一共 5 把)。哲学家有时要思考一会儿,不需要叉子;有时又要就餐,而一位哲学家只有同时拿到了左手边和右手边的两把叉子,才能吃到东西

  • 下面是每个哲学家的基本循环

        while (1) {
            think();        // 思考
            getforks();     // 拿起左右两把叉子
            eat();          // 就餐
            putforks();     // 放下两把叉子
        }
    
    * 辅助函数,用于定位哲学家 `p` 左右两边叉子的编号
    
        ```c
        int left(int p) { return p; }
        int right(int p) { return (p + 1) % 5; }

有问题的方案

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

void putforks() {
    sem_post(forks[left(p)]);
    sem_post(forks[right(p)]);
}
  • 如果每个哲学家都是先拿到左边的叉子然后再拿右边的叉子,那么就可能出现 死锁 的情况,即所有哲学家都拿起了左边的叉子,而一直等待右边的叉子

破除依赖

  • 为了解决这个问题,我们可以 修改某个或者某些哲学家的取餐顺序,这样总有哲学家能够吃到东西
void getforks() {
    if (p == 0) {   // 破除依赖
        sem_wait(forks[right(p)]);
        sem_wait(forks[left(p)]);
    }
    else {
        sem_wait(forks[left(p)]);
        sem_wait(forks[right(p)]);
    }
}

void putforks() {
    if (p == 0) {
        sem_post(forks[right(p)]);
        sem_post(forks[left(p)]);
    }
    else {
        sem_post(forks[left(p)]);
        sem_post(forks[right(p)]);
    }
}

测试

#include <stdio.h>
#include <assert.h>
#include <pthread.h>
#include <semaphore.h>

sem_t forks[5];
pthread_t philosophers[5];

int left(int p) { return p; }
int right(int p) { return (p + 1) % 5; }

void eat(int p) {
    printf("%d is eating !\n", p);
}

void getforks(int p) {
    if (p == 0) {   // 破除依赖
        sem_wait(&forks[right(p)]);
        sem_wait(&forks[left(p)]);
    }
    else {
        sem_wait(&forks[left(p)]);
        sem_wait(&forks[right(p)]);
    }
}

void putforks(int p) {
    if (p == 0) {
        sem_post(&forks[right(p)]);
        sem_post(&forks[left(p)]);
    }
    else {
        sem_post(&forks[left(p)]);
        sem_post(&forks[right(p)]);
    }
}


void* wantToEat(void *arg) {
    int p = *(int*)arg;
    getforks(p);
    eat(p);
    putforks(p);
    return NULL;
}

int main(int argc, char *argv[]) {
    for (int i = 0; i < 5; i++) {
        sem_init(&forks[i], 0, 1);
    }
    int id[5];
    for (int i = 0; i < 5; i++) {
        id[i] = i;
        pthread_create(&philosophers[i], NULL, wantToEat, (void*)&id[i]);
    }

    for (int i = 0; i < 5; i++) {
        pthread_join(philosophers[i], NULL);
    }

    return 0;
}

/* output:
4 is eating !
3 is eating !
2 is eating !
1 is eating !
0 is eating !
*/

如何实现信号量

  • 我们可以利用 条件变量 实现一个信号量

    #ifndef _ZEM_H_
    #define _ZEM_H_
    
    #include <pthread.h>
    
    typedef struct Zem_t {
        int value;
        pthread_mutex_t mutex;
        pthread_cond_t cond;
    } Zem_t;
    
    void Zem_init(Zem_t *s, int value) {
        s->value = value;
        pthread_mutex_init(&s->mutex, NULL);
        pthread_cond_init(&s->cond, NULL);
    }
    
    void Zem_wait(Zem_t *s) {
        pthread_mutex_lock(&s->mutex);
        while (s->value <= 0)
            pthread_cond_wait(&s->cond, &s->mutex);
        --s->value;
        pthread_mutex_unlock(&s->mutex);
    }
    
    void Zem_post(Zem_t *s) {
        pthread_mutex_lock(&s->mutex);
        ++s->value;
        pthread_cond_signal(&s->cond);
        pthread_mutex_unlock(&s->mutex);
    }
    
    void Zem_destroy(Zem_t *s) {
        pthread_mutex_destroy(&s->mutex);
        pthread_cond_destroy(&s->cond);
    }
    
    #endif