《Linux 环境下 C++ 条件变量与 POSIX 条件变量详解》

在多线程编程中,线程同步是一个至关重要的环节。当多个线程共享资源时,为了避免出现竞争条件和数据不一致等问题,我们需要采用合适的同步机制。条件变量作为线程同步的关键工具,在 Linux 环境下主要有 C++ 标准库提供的条件变量和 POSIX 标准定义的条件变量两种。本文将深入探讨这两种条件变量的原理、使用方法,并对比它们的优缺点,帮助你在实际项目中选择合适的同步方案。

一、条件变量是什么?为什么需要它?

想象一个场景:你是一家餐厅的厨师,需要根据订单来做菜。但你不想一直盯着订单系统看,而是希望当有新订单时,系统能主动通知你。这就是条件变量的核心思想 —— 线程等待某个条件满足时被唤醒,避免无效的轮询等待。

条件变量通常与互斥锁配合使用:

  1. 互斥锁用于保护共享资源
  2. 条件变量用于线程间的通知和等待

二、C++ 条件变量详解

来自cplusplus
在这里插入图片描述

  1. 功能:让线程阻塞,直到被其他线程通知后恢复执行。

  2. 使用依赖:配合 unique_lock(通过互斥锁 mutex 实现线程锁),调用 wait 系列函数时会锁定线程;需其他线程调用同一条件变量的通知函数(如 notify_one/notify_all )来唤醒阻塞。

  3. 变体关联:condition_variable 固定用 unique_lock 做等待;若要适配任意可锁类型,可用condition_variable_any 。

简单说,就是 C++ 里条件变量的基础定义、用法约束,以及和锁、通知机制的配合逻辑 。

在这里插入图片描述

函数参数作用实例
construcor创建条件变量std::condition_variable queue_condition;
destructor销毁条件变量std::condition_variable对象的析构函数会在对象生命周期结束时自动调用
void wait (unique_lock& lck);互斥锁将当前线程(Ick)陷入等待, 直到被唤醒queue_condition.wait(Ick);
bool wait (unique_lock& lck, Predicate pred);互斥锁,谓词将当前线程(Ick)陷入等待, 直到被唤醒queue_condition.wait(Ick, pred); 说明:若谓词 pred 返回 false 则阻塞,被通知且 pred 为 true 时解除阻塞,可应对虚假唤醒,等价于 while (!pred()) wait(lck); 逻辑。
void wait_for (unique_lock& lck, const chrono::duration<Rep,Period>& rel_time);互斥锁, 设定线程最多阻塞等待的时间当前线程的执行在相对时间内被阻塞,直到收到通知(如果先发生这种情况)。queue_condition.wait_for(lock, std::chrono::seconds(1) );(也可有上述带谓词重载版本)
wait_until (unique_lock& lck, const chrono::time_point<Clock,Duration>& abs_time);互斥锁,一个绝对时间点让当前持有互斥锁的线程阻塞,直到被通知或者到达指定的绝对时间点 。queue_condition.wait_until( lock, std::chrono::steady_clock::now() + std::chrono::seconds(2); );等待队列有空间,最多等待2秒 (也可有上述带谓词重载版本)
notify_one解除当前正在等待此条件的线程之一的阻塞状态。queue_condition.notify_one();
notify_all解除所有当前正在等待此条件的线程的阻塞状态。queue_condition.notify_all();

让我们看一个完整的生产者 - 消费者示例:

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <cstdlib>

// 共享缓冲区,生产者放入数据,消费者取出数据
std::queue<int> buffer;
// 互斥锁,保护对缓冲区的访问
std::mutex mutex;
// 条件变量,指示缓冲区非空(消费者等待此条件)
std::condition_variable not_empty;
// 条件变量,指示缓冲区未满(生产者等待此条件)
std::condition_variable not_full;
// 缓冲区最大容量
const int gcap = 10;

// 生产者线程函数
void product()
{
    int cnt;
    // 生产gcap个数据项
    for (int i = 0; i < gcap; i++)
    {
        // 生成随机数(0-99)
        cnt = rand() % 100;

        // 加锁保护缓冲区访问
        std::unique_lock<std::mutex> lock(mutex);

        // 等待缓冲区有空间(如果已满则阻塞)
        while(buffer.size() >= gcap)
            not_full.wait(lock);

        // 向缓冲区添加数据
        buffer.push(cnt);
        std::cout << "生产者产生数据:" << cnt << std::endl;
        std::cout << "当前产生数据总量:" << buffer.size() << std::endl;

        // 解锁,允许其他线程访问缓冲区
        lock.unlock();
        
        // 通知可能正在等待的消费者(缓冲区非空)
        not_empty.notify_one();
    }
}

// 消费者线程函数
void consume()
{
    int cnt;
    // 消费10个数据项
    for (int i = 0; i < 10; i++)
    {
        // 加锁保护缓冲区访问
        std::unique_lock<std::mutex> lock(mutex);

        // 等待缓冲区有数据(如果为空则阻塞)
        // 使用带谓词的wait版本,避免虚假唤醒
        not_empty.wait(lock, []{return !buffer.empty(); });

        // 从缓冲区取出数据
        cnt = buffer.front();
        buffer.pop();
        std::cout << "消费者消费数据:" << cnt << std::endl;
        std::cout << "当前生产者剩余数据:" << buffer.size() << std::endl;

        // 解锁,允许其他线程访问缓冲区
        lock.unlock();
        
        // 通知可能正在等待的生产者(缓冲区未满)
        not_full.notify_one();
    }
}

int main()
{
    // 创建生产者和消费者线程
    std::thread productor(product);
    std::thread consumer(consume);
    
    // 等待两个线程完成
    productor.join();
    consumer.join();

    return 0;
}

三、POSIX 条件变量详解

函数作用
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);初始化条件变量,为后续线程同步操作做准备。
int pthread_cond_destroy(pthread_cond_t *cond);销毁条件变量,释放相关资源,通常在不再需要条件变量时调用。
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);让线程阻塞等待条件满足,需配合互斥锁(pthread_mutex_t )使用,会原子性解锁互斥锁并等待,条件满足时重新加锁并返回。
int pthread_cond_signal(pthread_cond_t *cond);唤醒一个因该条件变量等待的线程,若有多个等待线程,按调度策略选一个唤醒。
pthread_cond_broadcast(pthread_cond_t *cond)唤醒所有因该条件变量等待的线程,常用于需所有等待线程响应的场景 。

POSIX 条件变量是基于 C 语言的底层实现,在 Linux 系统中广泛使用。下面是使用 POSIX 条件变量实现的相同生产者 - 消费者模型:

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

// 共享缓冲区:生产者存放数据,消费者获取数据
std::queue<int> buffer;
// 互斥锁:保护对缓冲区的访问
pthread_mutex_t mutex;
// 条件变量:缓冲区非空时通知消费者
pthread_cond_t not_empty;
// 条件变量:缓冲区未满时通知生产者
pthread_cond_t not_full;

// 生产者线程函数
void *product(void *arg)
{
    int cnt;
    // 生产10个数据
    for (int i = 0; i < 10; i++)
    {
        // 生成随机数作为产品
        cnt = rand() % 100;

        // 加锁:进入临界区
        pthread_mutex_lock(&mutex);

        // 检查缓冲区是否已满
        // 如果满了则等待"未满"信号,同时释放锁
        while (buffer.size() == 10)
            pthread_cond_wait(&not_full, &mutex);

        // 将产品放入缓冲区
        buffer.push(cnt);
        std::cout << "生产者产生数据:" << cnt << std::endl;
        std::cout << "当前缓冲区数据总量:" << buffer.size() << std::endl;

        // 通知消费者:缓冲区非空
        pthread_cond_signal(&not_empty);

        // 解锁:离开临界区
        pthread_mutex_unlock(&mutex);
    }

    return NULL;
}

// 消费者线程函数
void *consume(void *arg)
{
    int cnt;
    // 消费10个数据
    for (int i = 0; i < 10; i++)
    {
        // 加锁:进入临界区
        pthread_mutex_lock(&mutex);

        // 检查缓冲区是否为空
        // 如果空则等待"非空"信号,同时释放锁
        while (buffer.empty())
            pthread_cond_wait(&not_empty, &mutex);

        // 从缓冲区获取数据
        cnt = buffer.front();
        buffer.pop();
        std::cout << "消费者消费数据:" << cnt << std::endl;
        std::cout << "当前缓冲区剩余数据:" << buffer.size() << std::endl;

        // 通知生产者:缓冲区未满
        pthread_cond_signal(&not_full);

        // 解锁:离开临界区
        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}

int main()
{
    // 线程ID
    pthread_t productor, consumer;

    // 初始化互斥锁和条件变量
    pthread_mutex_init(&mutex, NULL);
    pthread_cond_init(&not_empty, NULL);
    pthread_cond_init(&not_full, NULL);

    // 创建生产者和消费者线程
    pthread_create(&productor, NULL, product, NULL);
    pthread_create(&consumer, NULL, consume, NULL);

    // 等待两个线程执行完毕
    pthread_join(productor, NULL);
    pthread_join(consumer, NULL);

    // 销毁互斥锁和条件变量
    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&not_empty);
    pthread_cond_destroy(&not_full);

    return 0;
}

用POSIX简单封装成c++条件变量

#pragma once

#include <iostream>
#include <pthread.h>
#include "Mutex.hpp"//也是c语言封装的互斥锁

namespace CondModule
{
    using namespace LockModule;

    class Cond
    {
    public:
        Cond()//初始化条件变量_cond。
        {
            int n = ::pthread_cond_init(&_cond, nullptr);
            (void)n;
        }
        void Wait(Mutex &mutex) // 让我们的线程释放曾经持有的锁!
        {
            int n = ::pthread_cond_wait(&_cond, mutex.LockPtr());
        }
        void Notify()//唤醒一个等待该条件变量的线程。
        {
            int n = ::pthread_cond_signal(&_cond);
            (void)n;
        }
        void NotifyAll()//唤醒所有等待该条件变量的线程。
        {
            int n = ::pthread_cond_broadcast(&_cond);
            (void)n;
        }
        ~Cond()
        {
            int n = ::pthread_cond_destroy(&_cond);
        }
    private:
        pthread_cond_t _cond;
    };
}

四、C++ 条件变量与 POSIX 条件变量对比

| | | |

维度C++ 条件变量 (std::condition_variable)POSIX 条件变量 (pthread_cond_t)
所属标准C++11 及以后标准库(<condition_variable>)POSIX 标准(<pthread.h>)
语言范式C++ 面向对象风格,与标准库深度集成C 语言过程式风格,手动管理资源
锁管理自动锁管理(配合std::unique_lock)手动锁操作(pthread_mutex_lock/unlock)
虚假唤醒处理通过谓词参数直接支持(wait(lock, predicate))需要手动编写循环检查条件
异常安全性内置异常处理,锁自动释放需手动确保异常时锁被释放
跨平台性支持所有 C++11 + 编译器,天然跨平台主要用于 POSIX 系统(Linux、macOS),Windows 需适配层
性能略高于 POSIX(依赖具体实现)底层系统调用,略低但可忽略

4.1 典型使用场景

优先选择 C++ 条件变量的场景

  1. 使用现代 C++ 的项目:如果项目基于 C++11 及以上标准,建议优先使用标准库提供的同步原语,保持代码一致性。
  2. 注重代码简洁性与安全性:C++ 条件变量通过 RAII 自动管理锁,减少手动操作导致的错误,且内置谓词参数避免虚假唤醒。
  3. 跨平台开发:无需担心底层系统差异,代码可在 Windows、Linux、macOS 等平台无缝运行。

优先选择 POSIX 条件变量的场景

  1. 嵌入式系统或资源受限环境:POSIX 接口更接近系统底层,某些嵌入式系统可能仅支持 POSIX 线程库。
  2. 高性能需求的系统级编程:对于性能敏感的应用(如网络服务器),POSIX 的手动锁管理可能提供更精细的控制。
  3. 与现有 POSIX 代码集成:如果项目中已大量使用 POSIX 线程、信号量等,保持一致性可降低维护成本。

如有错误,欢迎指正!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值