授课语音

线程同步之条件变量


1. 条件变量概述

条件变量(Condition Variable)是线程同步中的一种高级机制,用于解决线程间的通信问题,尤其是在某个线程需要等待某个条件满足时再继续执行的场景。它常常与互斥锁结合使用,以便在线程等待条件变化时,能够避免占用锁资源,释放锁直到条件满足。

条件变量本身不用于保护共享数据,而是通过与互斥锁的配合,提供一种等待和通知机制,帮助线程协调工作。


2. 条件变量的原理

条件变量允许线程等待某个特定的条件或事件发生。在等待期间,线程会释放与条件变量关联的互斥锁,直到被其他线程通知。通知的方式可以是“通知一个线程”或“通知所有线程”以唤醒等待线程继续执行。

2.1 主要操作

条件变量的操作主要有以下几种:

  • 等待(wait)

    • 线程调用wait操作时,它会释放关联的互斥锁并进入阻塞状态,直到被另一个线程通过notify操作唤醒。
    • wait通常是在一个循环中使用,以防止虚假唤醒(spurious wakeups)。
  • 通知(notify)

    • notify操作会唤醒一个等待线程,但并不释放锁。
    • 适用于只需要唤醒一个等待线程的情况。
  • 通知所有(notify_all)

    • notify_all操作会唤醒所有等待的线程,通常用于当多个线程等待同一条件时,都需要被唤醒的情况。

2.2 条件变量与互斥锁的结合

  • 互斥锁:条件变量通常与互斥锁一起使用。在使用条件变量时,线程首先获取互斥锁,检查条件是否满足。如果条件不满足,线程会调用wait函数,释放锁并进入等待状态;如果条件满足,线程会继续执行。
  • 通知机制:当某个线程修改了共享资源并满足条件时,它会调用notifynotify_all唤醒等待线程。

3. 条件变量的使用场景

条件变量通常用于以下几种场景:

  • 生产者-消费者问题:生产者线程生产数据并放入缓冲区,消费者线程从缓冲区取数据。消费者在缓冲区为空时等待,生产者在缓冲区满时等待。
  • 线程间依赖:多个线程之间存在某种依赖关系,某些线程必须等其他线程完成某些操作后才能继续执行。
  • 事件驱动:线程在等待某个事件或条件发生后才能继续执行。

4. 条件变量的优缺点

4.1 优点

  • 避免忙等待:与自旋锁相比,条件变量避免了繁忙的CPU资源消耗。当条件不满足时,线程会释放锁并进入阻塞状态,直到条件满足后唤醒。
  • 适用于复杂的同步逻辑:条件变量非常适合需要等待某个特定条件的场景,能够有效协调多个线程的执行顺序。

4.2 缺点

  • 需要与互斥锁配合使用:条件变量不能独立使用,它必须与互斥锁配合使用,这会增加编程的复杂性。
  • 可能出现虚假唤醒:线程在等待条件时可能被虚假唤醒,因此通常需要在wait操作中使用循环来检查条件。
  • 可能导致死锁:如果条件变量的使用不当,容易引发死锁,尤其是在多线程竞争锁的情况下。

5. 条件变量的实现(C语言示例)

在C语言中,条件变量通常与pthread_cond_t类型配合使用,并与互斥锁pthread_mutex_t一起使用。常见的操作包括pthread_cond_waitpthread_cond_signalpthread_cond_broadcast

5.1 示例:生产者-消费者模型

下面是一个使用条件变量的生产者-消费者模型的示例,生产者将数据放入缓冲区,消费者从缓冲区取数据:

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

#define BUFFER_SIZE 10

// 共享缓冲区
int buffer[BUFFER_SIZE];
int count = 0;  // 缓冲区中的项目数量

pthread_mutex_t mutex;            // 互斥锁
pthread_cond_t cond_producer;     // 生产者条件变量
pthread_cond_t cond_consumer;     // 消费者条件变量

// 生产者线程函数
void* producer(void* arg) {
    for (int i = 0; i < 20; i++) {
        pthread_mutex_lock(&mutex);  // 获取互斥锁

        // 如果缓冲区已满,等待消费者消费
        while (count == BUFFER_SIZE) {
            pthread_cond_wait(&cond_producer, &mutex);
        }

        // 生产数据并放入缓冲区
        buffer[count++] = i;
        printf("Produced: %d\n", i);

        // 通知消费者可以消费
        pthread_cond_signal(&cond_consumer);

        pthread_mutex_unlock(&mutex);  // 释放互斥锁
    }
    return NULL;
}

// 消费者线程函数
void* consumer(void* arg) {
    for (int i = 0; i < 20; i++) {
        pthread_mutex_lock(&mutex);  // 获取互斥锁

        // 如果缓冲区为空,等待生产者生产
        while (count == 0) {
            pthread_cond_wait(&cond_consumer, &mutex);
        }

        // 消费数据
        int item = buffer[--count];
        printf("Consumed: %d\n", item);

        // 通知生产者可以生产
        pthread_cond_signal(&cond_producer);

        pthread_mutex_unlock(&mutex);  // 释放互斥锁
    }
    return NULL;
}

int main() {
    pthread_t prod, cons;

    pthread_mutex_init(&mutex, NULL);
    pthread_cond_init(&cond_producer, NULL);
    pthread_cond_init(&cond_consumer, NULL);

    // 创建生产者和消费者线程
    pthread_create(&prod, NULL, producer, NULL);
    pthread_create(&cons, NULL, consumer, NULL);

    // 等待线程完成
    pthread_join(prod, NULL);
    pthread_join(cons, NULL);

    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&cond_producer);
    pthread_cond_destroy(&cond_consumer);

    return 0;
}

5.2 代码解析

  • 互斥锁pthread_mutex_t mutex 用于同步访问共享资源buffer
  • 条件变量pthread_cond_t cond_producerpthread_cond_t cond_consumer分别用于生产者和消费者线程的同步。
  • 生产者逻辑:当缓冲区满时,生产者线程会调用pthread_cond_wait,阻塞自己并释放互斥锁,直到消费者消费了数据。
  • 消费者逻辑:当缓冲区为空时,消费者线程会调用pthread_cond_wait,等待生产者生产数据。

6. 总结

  • 条件变量是一种高级的线程同步机制,允许线程在等待某个条件时释放互斥锁,避免占用系统资源。
  • 条件变量常用于需要线程等待某个条件或事件发生的场景,如生产者-消费者问题、线程间的依赖关系等。
  • 条件变量需要与互斥锁配合使用,并且使用时需要注意虚假唤醒和死锁等问题。
去1:1私密咨询

系列课程: