Linux 学习笔记(三十四):线程同步——条件变量与生产消费者模型

一、 为什么有了互斥锁还需要条件变量?

互斥锁解决了数据冲突,但无法解决"执行顺序"的问题。如果消费者一直抢锁去查仓库(死循环),会导致 CPU 空转(忙等待)。条件变量可以让线程在条件不满足时进入睡眠(阻塞),把 CPU 让出来,直到条件满足时被其他线程唤醒。

二、 核心 API 与易错点

  • pthread_cond_wait(cond, mutex):等待条件满足。

  • pthread_cond_signal(cond):唤醒一个等待的线程。

面试极度高频:为什么 wait 必须传互斥锁?

因为在进入睡眠前,必须释放锁;在醒来后,必须重新获取锁。把"释放锁"和"进入睡眠"合并成一个原子操作,可以防止在释放锁后、入睡前,错过了生产者发出的唤醒信号。

为什么判断条件必须用 while 循环?

因为存在"伪唤醒"(操作系统层面的原因)或多个线程被同时唤醒,醒来后必须再次检查队列状态,否则会导致越界访问。

三、 生产者消费者模型(321原则)

  • 3 种关系:生产者与生产者(互斥)、消费者与消费者(互斥)、生产者与消费者(同步+互斥)。

  • 2 个角色:生产者、消费者。

  • 1 个交易场所:阻塞队列(BlockingQueue)。

四、 实战:基于阻塞队列的并发控制

通过手写 BlockQueue 模板类,将生产者与消费者解耦。当队列满时,生产者阻塞;当队列空时,消费者阻塞。极大地提高了程序的健壮性和 CPU 利用率。

(选做:概述一下线程池的架构:提前创建好固定数量的线程,结合任务队列,一旦有任务丢进队列,线程就唤醒去抢任务执行,避免频繁创建销毁线程的开销)

自我实战操作

bash 复制代码
cd ~/linux_study
mkdir sync_test
cd sync_test
实战 1:手写一个阻塞队列
bash 复制代码
vim BlockQueue.hpp

(输入以下代码,结合了你之前学的锁和刚学的条件变量)

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

template <typename T>
class BlockQueue {
public:
    BlockQueue(int cap = 10) : _cap(cap) {
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_cond_pro, nullptr); // 给生产者准备的条件变量
        pthread_cond_init(&_cond_con, nullptr); // 给消费者准备的条件变量
    }
    ~BlockQueue() {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_cond_pro);
        pthread_cond_destroy(&_cond_con);
    }

    // 生产者接口
    void Push(const T& in) {
        pthread_mutex_lock(&_mutex);
        // 如果队列满了,生产者就去睡觉
        while (_q.size() >= _cap) {
            pthread_cond_wait(&_cond_pro, &_mutex);
        }
        _q.push(in);
        // 生产了东西,唤醒消费者
        pthread_cond_signal(&_cond_con);
        pthread_mutex_unlock(&_mutex);
    }

    // 消费者接口
    void Pop(T* out) {
        pthread_mutex_lock(&_mutex);
        // 如果队列空了,消费者就去睡觉
        while (_q.empty()) {
            pthread_cond_wait(&_cond_con, &_mutex);
        }
        *out = _q.front();
        _q.pop();
        // 消费了东西,唤醒生产者
        pthread_cond_signal(&_cond_pro);
        pthread_mutex_unlock(&_mutex);
    }

private:
    std::queue<T> _q;
    int _cap;
    pthread_mutex_t _mutex;
    pthread_cond_t _cond_pro;
    pthread_cond_t _cond_con;
};
实战 2:用两个线程测试阻塞队列
bash 复制代码
vim main.cc

(输入以下代码)

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include "BlockQueue.hpp"

void* Consumer(void* arg) {
    BlockQueue<int>* bq = (BlockQueue<int>*)arg;
    while (true) {
        int data;
        bq->Pop(&data);
        std::cout << "消费者消费了数据: " << data << std::endl;
        sleep(1); // 模拟消费处理耗时,消费慢,生产快
    }
    return nullptr;
}

void* Producer(void* arg) {
    BlockQueue<int>* bq = (BlockQueue<int>*)arg;
    int data = 1;
    while (true) {
        bq->Push(data);
        std::cout << "生产者生产了数据: " << data << std::endl;
        data++;
        usleep(200000); // 生产快
    }
    return nullptr;
}

int main() {
    BlockQueue<int>* bq = new BlockQueue<int>(5); // 仓库容量只有5
    pthread_t c, p;
    pthread_create(&c, nullptr, Consumer, (void*)bq);
    pthread_create(&p, nullptr, Producer, (void*)bq);

    pthread_join(c, nullptr);
    pthread_join(p, nullptr);
    return 0;
}

编译运行:

bash 复制代码
g++ main.cc -o main -lpthread
./main

👉 现象 :因为消费者睡1秒,生产者0.2秒放一个,很快队列就满了(5个)。此时生产者的 while 循环生效,进入 wait 睡眠,不再疯狂刷屏。等消费者取走一个,生产者才醒过来放一个。这就是完美的同步控制!

相关推荐
蓝祁子yoky1 小时前
Qt学习日记 Day11:多线程(二)+线程同步/互斥
学习
土豆198910211 小时前
2026.10.27 笔记
笔记
L0208241 小时前
Kubernetes Service 与 kube‑proxy 深度详解
运维·算法·云原生·容器·贪心算法·kubernetes
宝绿特塑业再生科技1 小时前
尺寸稳定性重构:改性再生HDPE破解户外家具热胀冷缩困局
经验分享·学习·生活·户外家具
无心水1 小时前
5、信息安全技术基础知识 核心学习笔
人工智能·学习·软考高级·架构师·软考·高级架构师·软考架构师
艾莉丝努力练剑1 小时前
【AI大模型接入SDK】Trae生成main方法
网络·人工智能·网络协议·http·架构·1024程序员节
fanged1 小时前
蓝牙学习5(BLE Audio)(TODO)
学习
根目录下的猫1 小时前
ArkTS 代码中如何添加作者信息
linux·运维·ubuntu