回顾:

临界段问题

  • 在并发程序中,资源是共享的
  • 临界段资源:部分资源需要互斥使用
  • 利用“锁”的思想保护临界段资源

并发的需求

  • 互斥执行、同步执行

一、信号量的定义

控制并发的信号灯-信号量(semaphore)

  • 信号量是有一个整数值的对象
  • 信号量可以代替锁/条件变量
  • 信号量可以并发过程中控制资源的使用情况

信号量是包含一个整数值的对象。

  • 我们可以用sem_wait()和sem_post()来操作
  • 信号量的初始值决定其行为,因此需要初始化
    • 例:初始化为1,第二个参数0标识信号量是线程共享的
#include <semaphore.h>
sem_t s;
sem_init(&s, 0, 1); // initialize s to the value 1
  • sem_wait():又称为P()操作

    • 信号量的值减1
    • 结果如果小于0,则使调用的线程挂起直到被其他线程调用 sem_post()唤醒
    int sem_wait(sem_t *s) { 
    	decrement the value of semaphore s by one; 
    	wait if value of semaphore s is negative 
    }
    
  • sem_post():又称为V()操作

    • 将信号量的值加1
    • 如果有其他线程等待被唤醒,则唤醒其中一个

二、二值信号量(锁)

  • 用信号量作为锁

    • 将临界区环绕
    sem_t m; 
    sem_init(&m, 0, X); // initialize semaphore to X; what should X be? 
    sem_wait(&m); 
    //critical section here 
    sem_post(&m);
    

X = 1

三、信号量作为条件变量

  • 一个线程等待条件成立,另一个线程修改条件并发信号

    • 用信号量值的变化作为状态改变信号
    #include <stdio.h>
    #include <pthread.h>
    #include <semaphore.h>
    
    sem_t s; // 定义信号量
    
    void *child(void *arg) { 
        printf("child\n"); 
        sem_post(&s); // signal here: child is done 
        return NULL; 
    }
    
    int main(int argc, char *argv[]) { 
        sem_init(&s, 0, X); // X 应该是 0,表示信号量初始值为 0
        printf("parent: begin\n"); 
    
        pthread_t c; 
        pthread_create(&c, NULL, child, NULL); // 创建子线程
    
        sem_wait(&s); // wait here for child 
        printf("parent: end\n"); 
    
        return 0; 
    }
    

X = 0

四、生产者/消费者问题

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

#define MAX 10  // 缓冲区的最大大小
int buffer[MAX]; 
int fill = 0; 
int use = 0; 

sem_t empty; // 记录空槽位的信号量
sem_t full;  // 记录满槽位的信号量

void put(int value) { 
    buffer[fill] = value; // 将值放入缓冲区
    fill = (fill + 1) % MAX; // 更新填充指针
}

int get() { 
    int tmp = buffer[use]; // 从缓冲区取出值
    use = (use + 1) % MAX; // 更新使用指针
    return tmp;
}

void *producer(void *arg) { 
    int i; 
    for (i = 0; i < 10; i++) { // 假设生产 10 个产品
        sem_wait(&empty); // 等待空槽位
        put(i);   // 生产产品
        sem_post(&full);  // 增加满槽位的计数
    } 
}

void *consumer(void *arg) { 
    int i, tmp = 0; 
    while (tmp != -1) { 
        sem_wait(&full);  // 等待满槽位
        tmp = get();  // 消费产品
        sem_post(&empty); // 增加空槽位的计数
        printf("%d\n", tmp); // 打印消费的产品
    } 
}

int main(int argc, char *argv[]) {
    // 初始化信号量
    sem_init(&empty, 0, MAX); // 初始化:MAX 个缓冲区空
    sem_init(&full, 0, 0);    // 初始化:0 个缓冲区满
    
    // 创建生产者和消费者线程
    pthread_t prod_thread, cons_thread;
    pthread_create(&prod_thread, NULL, producer, NULL);
    pthread_create(&cons_thread, NULL, consumer, NULL);

    // 等待线程结束
    pthread_join(prod_thread, NULL);
    pthread_join(cons_thread, NULL);
    
    // 销毁信号量
    sem_destroy(&empty);
    sem_destroy(&full);
    
    return 0; 
}

是否可行?在什么情况下可能 产生问题?

一个简单规则:别忘了加锁

sem_t empty; // 记录空槽位的信号量
sem_t full;  // 记录满槽位的信号量
sem_t mutex; // 互斥信号量,用于保护临界区
void *producer(void *arg) { 
    int i; 
    for (i = 0; i < loops; i++) { // 假设 loops 是预定义的循环次数
        sem_wait(&mutex); // 进入临界区
        sem_wait(&empty); // 等待空槽位
        put(i);           // 生产产品
        sem_post(&full);  // 增加满槽位的计数
        sem_post(&mutex); // 离开临界区
    }  
}
void *consumer(void *arg) { 
    int i; 
    for (i = 0; i < loops; i++) { // 假设 loops 是预定义的循环次数
        sem_wait(&full);  // 等待满槽位
        sem_wait(&mutex); // 进入临界区
        int tmp = get();  // 从缓冲区获取产品  
        sem_post(&empty); // 增加空槽位的计数
        sem_post(&mutex); // 离开临界区
        printf("%d\n", tmp); // 打印消费的产品
    } 
}

还有问题吗?

将锁精准的加到临界区附近。

sem_t empty; // 记录空槽位的信号量
sem_t full;  // 记录满槽位的信号量
sem_t mutex; // 互斥信号量,用于保护临界区
void *producer(void *arg) { 
    int i; 
    for (i = 0; i < loops; i++) { // 假设 loops 是预定义的循环次数
        sem_wait(&empty); // line p1: 等待空槽位
        sem_wait(&mutex); // line p1.5: 进入临界区,确保互斥访问
        put(i);   // line p2: 生产产品并放入缓冲区
        sem_post(&mutex); // line p2.5: 离开临界区
        sem_post(&full);  // line p3: 增加满槽位的计数
    } 
}

五、读者/写者锁

  • 一个线程写的时候,其他线程不能读或写
  • 一个线程读的时候,其他线程可以读
  • 例:链表操作
    • insert():插入节点(写)
    • lookup():查找节点(读)

enter image description here

enter image description here

“信号量”小结

  • 控制共享资源的访问:互斥/同步
  • 信号量作为锁/条件变量
  • 生产者/消费者问题:2个信号量,分别描述空/满的缓冲块 数量,不能同时放或取
  • 读者/写者问题:1个读锁,1个写锁,1个计数器