操作系统笔记(12)-条件变量

简介

  • 在很多情况下,线程需要检查某一条件满足之后,才会继续运行,比如说主线程可以等待子线程执行完后再继续执行

  • 条件变量可以令线程在条件 未满足 时进入 睡眠 ,也支持在任务完成后 唤醒 另外的线程

  • pthread_cond_wait 调用之前必须持有锁,因为它会释放锁然后使线程进入 cond 等待队列,当使用 pthread_cond_signalpthread_cond_broadcast 时,会将线程从 cond 队列移动到 mutex 队列,再次获取锁后执行相关任务

父线程等待子线程:使用条件变量

  • 利用 条件变量 实现父线程等待子线程

    #include <stdio.h>
    #include <stdlib.h>
    #include <pthread.h>
    #include <unistd.h>
    
    int done = 0;
    
    // 另一种初始化的方式
    pthread_cond_t c = PTHREAD_COND_INITIALIZER;
    pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;
    
    void* child(void *arg) {
        pthread_mutex_lock(&m);
        done = 1;
        sleep(1);
        printf("child\n");
        pthread_cond_signal(&c);
        pthread_mutex_unlock(&m);
        return NULL;
    }
    
    void* thr_join(void *arg) {
        pthread_mutex_lock(&m);
        while (done == 0)
            pthread_cond_wait(&c, &m);
        printf("parent\n");
        pthread_mutex_unlock(&m);
        return NULL;
    }
    
    int main(int argc, char* argv[]) {
        printf("parent : begin\n");
        pthread_t p;
        pthread_create(&p, NULL, child, NULL);
        thr_join(NULL);
        printf("parent : end\n");
        pthread_mutex_destroy(&m);
        pthread_cond_destroy(&c);
        return 0;
    }
    
    /* output:
    parent : begin
    child
    parent
    parent : end
    */
  • 关注点: signal() 应该在 unlock() 之前还是之后,不同的调用顺序可能有不同的结果。查阅相关资料发现,如果 signal() 之后 unlock() ,可以保证低优先级线程不会抢占高优先级线程;如果 signal() 之前 unlock() ,可以降低内核开销,提高效率

  • 结论:如果在意调度行为预测,最好先 signal()unlock() ,如果在应用层不在意线程优先级和执行顺序,则可以先 unlock()

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

  • 假设有一个或多个生产者线程和一个或多个消费者线程

  • 生产者把生成的数据放入缓冲区,消费者从缓冲区中取走数据项,以某种方式消费

  • 因为有界缓冲区是共享资源,所以我们必须通过 同步机制 来访问它,以免产生 竞态条件

代码实现

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>

#define BUFSIZE 16

pthread_cond_t fill, empty;
pthread_mutex_t m;

int buffer[BUFSIZE];
int fill_ptr;
int use_ptr;
int counter;

// 向缓冲区增加一项
void put(int value) {
    buffer[fill_ptr] = value;
    fill_ptr = (fill_ptr + 1) % BUFSIZE;
    counter++;
}

// 向缓冲区取出一项
int get() {
    int value = buffer[use_ptr];
    use_ptr = (use_ptr + 1) % BUFSIZE;
    counter--;
    return value;
}

// 生产者线程
void* producer(void *arg) {
    int loops = *(int*)arg;
    for (int i = 0; i < loops; i++) {
        pthread_mutex_lock(&m);
        while (counter == BUFSIZE)
            pthread_cond_wait(&empty, &m);
        put(i);
        printf("put : %d\n", i);
        pthread_cond_signal(&fill);
        pthread_mutex_unlock(&m);
    }
}

// 消费者线程
void* consumer(void *arg) {
    int loops = *(int*)arg;
    for (int i = 0; i < loops; i++) {
        pthread_mutex_lock(&m);
        while (counter == 0) 
            pthread_cond_wait(&fill, &m);
        int value = get();
        pthread_cond_signal(&empty);
        printf("get : %d\n", value);
        pthread_mutex_unlock(&m);
    }
}


int main(int argc, char* argv[]) {
    if (argc != 2) {
        puts("Please input loops");
        return 0;
    }
    pthread_cond_init(&fill, NULL);
    pthread_cond_init(&empty, NULL);
    pthread_mutex_init(&m, NULL);

    int loops = atoi(argv[1]);
    pthread_t pp, pc;
    pthread_create(&pp, NULL, producer, (void*)&loops);
    pthread_create(&pc, NULL, consumer, (void*)&loops);


    pthread_join(pp, NULL);
    pthread_join(pc, NULL);

    pthread_cond_destroy(&fill);
    pthread_cond_destroy(&empty);
    pthread_mutex_destroy(&m);
    return 0;
}
  1. 使用两个条件变量是为了防止如一个生产者多个消费者时,消费者唤醒消费者 会导致所有线程睡眠的问题

  2. 使用 while 循环检查而不是 if 的原因是当一个线程准备取出缓冲区的一个值时,被另一个线程强先取出,这时缓冲区为空,再取就出问题了,因此采用 while 循环检查可以有效避免 虚假唤醒 问题