Linux 线程同步与互斥——条件变量与线程同步

互斥锁解决了多个线程同时修改共享数据的问题,但多线程程序还有另一类需求:有些线程并不是不能访问共享资源,而是当前条件不满足,暂时什么也做不了。

比如,一个线程负责从队列中取任务。如果队列是空的,线程取不到任何东西。它应该怎么办?一直循环检查队列,还是等有任务时再继续执行?

这就涉及线程同步和条件变量。

一、互斥和同步不是一回事

互斥和同步经常一起出现,但它们解决的问题不同。

互斥主要解决访问冲突。 多个线程使用同一份共享数据时,需要限制同时进入临界区的线程数量,避免数据被破坏。

同步主要解决执行顺序和条件等待。 某个操作必须等到特定条件满足之后才能继续,例如消费者必须等队列中有数据,才能取出任务。

假设有两个线程,一个负责生产数据,另一个负责消费数据。

  • 互斥要求它们访问队列时不能同时破坏队列结构。
  • 同步要求消费者在队列为空时等待,生产者放入数据后再通知消费者。

只有互斥,消费者仍然不知道什么时候该开始工作;只有同步,却没有互斥,多个线程同时修改队列时又可能产生冲突。

所以,实际的多线程程序经常需要同时使用互斥锁和条件变量。

二、什么是条件变量?

条件变量可以让线程在某个条件不满足时进入等待状态,并在其他线程改变条件后被唤醒。

Pthreads 提供了以下接口:

cpp 复制代码
#include <pthread.h>

// 初始化与销毁
pthread_cond_init(&cond, nullptr);
pthread_cond_destroy(&cond);

// 等待条件
pthread_cond_wait(&cond, &mutex);

// 唤醒一个等待线程
pthread_cond_signal(&cond);

// 唤醒所有等待线程
pthread_cond_broadcast(&cond);

条件变量通常和互斥锁配合使用。共享条件需要由互斥锁保护,线程通过条件变量等待条件变化。

其中,pthread_cond_signal() 用于唤醒一个等待线程,pthread_cond_broadcast() 则用于唤醒所有等待该条件变量的线程。

不过,唤醒并不代表线程一定能够马上继续执行。被唤醒的线程还需要重新获得互斥锁,才能从 pthread_cond_wait() 返回并继续访问受到保护的共享数据。

三、pthread_cond_wait 为什么需要互斥锁?

pthread_cond_wait() 的接口比较特殊:

cpp 复制代码
int pthread_cond_wait(
    pthread_cond_t* cond,
    pthread_mutex_t* mutex
);

调用它时,当前线程必须已经持有传入的互斥锁。

当线程进入条件等待时,这个函数会原子地释放互斥锁并进入等待状态;当线程被唤醒后,它会重新竞争这把锁,并在成功获得锁之后返回。

这里的"原子地释放并等待"非常重要。

假设不使用 pthread_cond_wait(),而是自己先解锁,再调用等待函数,中间就可能出现一个时间窗口:其他线程已经修改了条件并发出通知,但当前线程还没有真正开始等待。这样一来,通知就可能被错过,当前线程可能一直等下去。

条件等待接口正是为了解决这种衔接问题而设计的。

四、一个简单的条件变量例子

下面用一个 ready 变量代表条件是否满足。工作线程必须等到 ready 变成 true,才能继续执行。

cpp 复制代码
#include <iostream>
#include <pthread.h>

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

bool ready = false;

void* worker(void* arg)
{
    pthread_mutex_lock(&mutex);

    while (!ready)
    {
        pthread_cond_wait(&cond, &mutex);
    }

    std::cout << "condition is ready" << std::endl;

    pthread_mutex_unlock(&mutex);
    return nullptr;
}

int main()
{
    pthread_t tid;

    int ret = pthread_create(&tid, nullptr, worker, nullptr);
    if (ret != 0)
    {
        std::cerr << "pthread_create failed: "
                  << ret << std::endl;
        return 1;
    }

    pthread_mutex_lock(&mutex);

    ready = true;
    pthread_cond_signal(&cond);

    pthread_mutex_unlock(&mutex);

    pthread_join(tid, nullptr);
    return 0;
}

这里的执行过程是:

  1. 工作线程获取互斥锁,检查 ready。
  2. 如果 ready 还是 false,就调用 pthread_cond_wait()。
  3. 主线程把 ready 改成 true,并发送通知。
  4. 工作线程被唤醒,重新获得互斥锁。
  5. 工作线程再次检查条件,发现条件已经满足,然后继续执行。

即使主线程发出通知时,工作线程还没有真正进入等待,程序也不会因此丢失状态。因为 ready 这个共享条件在锁的保护下被修改,工作线程随后获得锁时仍然可以看到条件已经满足。

五、为什么等待条件时应该用 while,而不是 if?

条件变量编程中,一个特别重要的细节是:被唤醒不等于条件一定满足。

原因主要有两个。

第一,线程可能在条件仍然不满足时被唤醒,例如发生了虚假唤醒。

第二,即使条件曾经满足,也可能在当前线程重新获得锁之前,被其他线程再次改变。

例如,消费者 A 和消费者 B 同时等待队列中的数据。当生产者放入一个任务并唤醒两个消费者后,消费者 A 先获得锁,把这个任务取走。消费者 B 随后获得锁时,队列已经再次变空。

如果消费者 B 只检查一次条件就继续取任务,就可能访问空队列。

因此,推荐的写法是:

cpp 复制代码
pthread_mutex_lock(&mutex);

while (!condition)
{
    pthread_cond_wait(&cond, &mutex);
}

// 条件满足,执行对应操作

pthread_mutex_unlock(&mutex);

每次从 pthread_cond_wait() 返回之后,都重新检查条件。只要条件还不满足,就继续等待。

六、条件变量的通知应该怎么写?

一个常见的使用模式是:修改条件的线程也需要先加锁。

cpp 复制代码
pthread_mutex_lock(&mutex);

// 修改共享状态,使条件发生变化
condition = true;

pthread_cond_signal(&cond);

pthread_mutex_unlock(&mutex);

等待线程则采用这样的结构:

cpp 复制代码
pthread_mutex_lock(&mutex);

while (!condition)
{
    pthread_cond_wait(&cond, &mutex);
}

// 条件满足,继续工作

pthread_mutex_unlock(&mutex);

核心并不是一定要先通知还是后通知,而是共享条件需要受到互斥锁保护,等待方必须在锁的保护下检查条件,通知方也需要在锁的保护下修改条件。

这样才能避免共享状态被并发修改,并降低线程错过状态变化的风险。

七、条件变量也需要管理生命周期

条件变量可以使用静态初始化:

cpp 复制代码
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

也可以动态初始化:

cpp 复制代码
pthread_cond_t cond;
pthread_cond_init(&cond, nullptr);

对于动态初始化的条件变量,在不再使用后需要调用 pthread_cond_destroy() 销毁。销毁之前,要确保没有线程仍然等待在该条件变量上,也没有线程再尝试使用它。

同样,互斥锁也必须在合适的时候销毁。条件变量和互斥锁往往成对使用,但两者仍然是独立的对象,应分别管理它们的生命周期。

八、总结

互斥锁保证多个线程不会同时破坏共享数据,条件变量则负责让线程在条件不满足时等待,并在状态变化后继续执行。

使用条件变量时,要牢牢记住几个要点:共享条件需要互斥锁保护;pthread_cond_wait() 会在等待时释放锁、返回前重新获取锁;等待条件应该使用 while 反复检查,而不能简单地把收到通知当成条件已经成立。

相关推荐
damoluomu1 小时前
国产 Java CMS 的协议和坑,讲清楚
java·开发语言
杜 硕1 小时前
从零开始的C++生活——第二讲——03
c++·生活
Wang's Blog1 小时前
Java 项目实战: 交友项目 - 黑名单:分页查询黑名单
java·开发语言·交友
乐橙开放平台1 小时前
不用换摄像头,如何用乐橙把海康、大华、宇视统一到一个界面
笔记·物联网·自动化·音视频·智能家居
mklpo1471 小时前
python3:如何按照实际执行需求导入模块,而不是启动时统一导入?(Python3.15)。2026-10-20
开发语言·人工智能·python
蚰蜒螟1 小时前
从 Java write 到磁盘扇区:一次写入操作的完整内核穿越之旅
java·开发语言
张文君1 小时前
vmware克隆后ubuntu26.04 ip怎么更改
linux·运维·服务器
en.en..1 小时前
volatile 关键字整理(嵌入式 C 标准)
java·开发语言
凯凯姜2 小时前
Vue 3 深入学习笔记 16:API 请求封装与统一错误处理
vue.js·笔记·学习