Linux线程4.0-线程同步-条件变量cond,深入生产者-消费者模型。

@bit::Shadow
✧(≖ ◡ ≖✿

目录

问题:线程不同步下单线程占用CPU过度,其他线程无法获取平均分配的资源。

解决办法:

条件变量

定义

销毁

等待

唤醒

注意:对于pthread_cond_signal()唤醒顺序是不一定的。

[生产者------消费者 条件变量实现](#生产者——消费者 条件变量实现)

☆生产者------消费者模型

生产者、消费者关系的321

[☆☆☆阻塞队列Blocking Queue 复习时非常值得重新完整的再实现一次](#☆☆☆阻塞队列Blocking Queue 复习时非常值得重新完整的再实现一次)

结构图示:

队列

EQueue()

Pop()

视频演示

生产者消费者模型的意义

复盘:

核心点:

条件变量:

POSIX信号量

回顾

[接口sem_init() sem_detroy() sem_wait() sem_post()](#接口sem_init() sem_detroy() sem_wait() sem_post())

信号量与条件变量的对比

条件变量的封装

信号量的极简封装


问题:线程不同步下单线程占用CPU过度,其他线程无法获取平均分配的资源。

单线程**++对锁频繁地申请与释放++** ,由于此线程"距离近",CPU切换调度相对对而言耗费大。导致单个线程被过度调用而非我们预期的各个线程平均分配机会。而导致其他线程的"饥饿问题"。

解决办法:

使用线程同步控制------条件变量 / POSIX信号量,规范线程调取资源的顺序。

条件变量

让一个线程在某个条件不满足时"休眠等待",其他线程在适当时间对其唤醒。

定义

全局定义

cpp 复制代码
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

局部定义需要调用初始化函数

cpp 复制代码
int pthread_cond_init(pthread_cond_t* restrict cond, \
                      pthread_condattr_t* restrict condattr);

🔗(C++进阶4.0)restrict关键字自C99引入,表示该指针是访问其指向数据的唯一访问方式。

使用

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

销毁

cpp 复制代码
int pthread_cond_destroy(pthread_cond_t* cond);

等待

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

唤醒

适当条件下,其他线程对目标线程(wait)的唤醒。

cpp 复制代码
//全部唤醒
int pthread_cond_broadcast(pthread_cond_t* cond);
//指定cond唤醒
int pthread_cond_signal(pthread_cond_t* cond);

注意:对于pthread_cond_signal()唤醒顺序是不一定的。

生产者------消费者 条件变量实现

☆生产者------消费者模型

  • 生产者:多位,负责生产果实。
  • 消费者:多位,负责消耗果实。
  • 交易场所:位于生产者,消费者之间的中间商。

生产者、消费者关系的321

3种关系:

  1. 消费者之间:竞争,互斥。
  2. 生产者之间:竞争,互斥。
  3. 生产者与消费者之间:同步(同步资源情况),互斥(中间商不能同时访问)

2个角色:生产者,消费者都是由线程承担的角色。

1个交易场所:由特定结构构成的"内存"空间。

☆☆☆阻塞队列Blocking Queue 复习时非常值得重新完整的再实现一次

ps:理论+纯手搓。

使用阻塞队列模拟实现生产者消费者模型。

作为一种实现生产者、消费者之间通信的数据结构。其与普通的队列区别在于,++队列空时"获取端"会被阻塞,消费者被阻塞,暂停消耗;队列满时"输入端"会被阻塞,生产者被阻塞,暂停生产++。

结构图示:

圆环区域内各个区间存储数据,当实现线程间灵活通信。

队列

使用queue即: std::queue<T, class Container = std::dequeue<T> 做封装

queue是一个容器适配器,其底层是 deque<T> 封装的结果。

cpp 复制代码
template<typename T>
class BlockQueue
{
    public:


    private:
        size_t _capacity;
        //仅是手动限定,queue可自动扩容。这是使用 条件变量 而非 POSIX信号量 的原因。
        std::queue<T> _BlockQueue;//无reserve

        pthread_cond_t _IsFull;
        pthread_cond_t _UnEnough;

        //锁为cond的等待
        pthread_mutex_t _EQMutex;
        //生产的标志,被用于消费者识别唤醒。 可以使用size()代替啊/err size有上限且逻辑错误
        int psleep;
        int csleep;

        //消费者的锁🔒
        pthread_mutex_t _PpMutex;
};

EQueue()

入队列:生产者产生果实。

主操作------主操作边界条件 (像:满 空 打开失败等等)------ 加锁/解锁 ------外部依赖(像:EQueue线程-考虑Pop与线程的通信"唤醒条件")------内部依赖(线程池的单例模式:GetInstance() { if(inc == nullptr) //..... inc = new ThreadPool<T>()"内部调用构造函数" };若单if下可能快进程先new,后进程判断nullptr失败。所以需要双重if

cpp 复制代码
bool EQueue(T val)
{
    /*
    1.条件队列未满(<_capacity)
    2.入队要原子操作 
    3.唤醒标记📌作为消费者由于 空 而等待(阻塞)。待生产者生产后立即 唤醒 的标志
    */

    //入队
    //在此处加锁 若锁错误,则打印输出错误
    pthread_mutex_lock(&_EQMutex);

    if(_BlockQueue.size() >= _capacity)  //可替换为while防止伪等待 健壮性考虑
    {
        // 队列满了,入的应该等待处理
        // pthread_wait?/err
        printf("生产满了,正在等待...\n");
        psleep++;
        pthread_cond_wait(&_IsFull, &_EQMutex);
        //等待结束(必然是消费者的唤醒)
        printf("生产者被唤醒!\n");
        psleep--;
        // 满了就应该等待,这时若消费者消耗了就应该立即启动
    }
        //入队列(定然未满)
        _BlockQueue.push(val);
        std::cout << "生产了" << _BlockQueue.back() << std::endl;
        if(csleep)
        {
            std::cout << "消费者被唤醒成功" << std::endl;
            pthread_cond_signal(&_UnEnough);
        }
        // pthread_mutex_lock使用自己封装的C Lock

        pthread_mutex_unlock(&_EQMutex);  // 解锁🔓

        return true;
}

Pop()

出队列

cpp 复制代码
bool Pop()
{
    //出队列
    pthread_mutex_lock(&_PpMutex);
    if(_BlockQueue.size() <= 0)   // 可替换为while防止伪等待
    {
        //小于等于0 要等待
        printf("消耗殆尽正在等待...\n");
        csleep++;
        pthread_cond_wait(&_UnEnough, &_PpMutex);
        csleep--;
    }
    //
    std::cout << _BlockQueue.front() << "被消耗" << std::endl;
    _BlockQueue.pop();
    if(psleep)
    {
        std::cout << "生产者唤醒成功" << std::endl;
        pthread_cond_signal(&_IsFull);//唤醒生产者 若没有等待的生产者你不炸了?所以要使用特定的sleep标记
    }
    pthread_mutex_unlock(&_PpMutex);
    return true;
}

视频演示

生产者消费者

gitee.com内test.cc、Thread_cond.hpp即可实现调试现象。

生产者消费者模型的意义

为什么要有生产端、消费端模型?

1.支持忙闲不均衡。

2.提高效率。

3.生产过程与消费过程解耦。

复盘:

生产端: 消费端:

cond条件变量实现了消费者、生产者之间的通信,决定了何时唤醒对方。(使用psleep、csleep来作为等待的标志)。

核心点:

掌握多线程下原子性精准加锁\解锁,掌握设计模板(主操作------主操作边界条件 (像:满 空 打开失败等等)------ 加锁/解锁 ------外部依赖(像:EQueue线程-考虑Pop与线程的通信"唤醒条件")------内部依赖(线程池的单例模式:GetInstance() { if(inc == nullptr) //..... inc = new ThreadPool<T>()"内部调用构造函数" };若单if下可能快进程先new,后进程判断nullptr失败。所以需要双重if),熟稔于心:通信psleep、csleep的精妙设计。

cpp 复制代码
static ThreadPool<T> *GetInstance()
{
    if (inc == nullptr)
    {
        LockGuard lockguard(_lock);

        LOG(LogLevel::DEBUG) << "获取单例....";
        if (inc == nullptr)
        {
            LOG(LogLevel::DEBUG) << "首次使用单例, 创建之....";
            inc = new ThreadPool<T>();
            inc->Start();
        }
    }

    return inc;
}

条件变量:

条件变量的初始化与锁完全一致均是 定义的地址, nullptr(默认属性) 。

锁的lock、unlock条件变量的wait、signal、broadcast参数均是指针类型。

使用数组模拟回环队列 ==》queuesize % capacity


POSIX信号量

回顾

信号量又称信号灯与信号(进程)无任何关系,其本质是一个计数器,衡量资源的预订机制,信号量又有二元信号量以及多元信号量,对信号量的P(--)操作,V(++)操作均是原子的。

接口sem_init() sem_detroy() sem_wait() sem_post()

信号量的初始化

cpp 复制代码
int sem_init(sem_t* sem, int pshared, unsigned int value);

sem:要设置的信号量。

pshared:0表示线程间共享,非0表示进程间共享。

value:信号量初始值。

信号量的销毁

cpp 复制代码
int sem_destroy(sem_t* sem);

信号量的等待/减少

cpp 复制代码
int sem_wait(sem_t* sem);//P(--)

是信号量的 -- 操作,代表信号量维护的资源 即将被使用。

若信号量的值为0,则++阻塞等待++直到有资源可用。 // 同一信号量的V()操作。

信号量的释放/增加

cpp 复制代码
int sem_post(sem_t* sem);//V(++)

是信号量的++操作,代表信号量维护的可用资源 又增加了。

若存在于信号量处等待的线程,线程会被释放。(顺序无明确规定但通常是FIFO/优先级调度)

Equeue()

由于sem_wait()自带的阻塞作用因此大大简化了代码.

sem_post(&Pop_sem);//V(++)通知消费者信号量

cpp 复制代码
void EQueue(T val)
{
    //入队
    sem_wait(&Equeue_Sem);//P(--)
    pthread_mutex_lock(&_EQMutex);

    _BlockQueue.push(val);
    std::cout << "生产了" << _BlockQueue.back() << std::endl;

    pthread_mutex_unlock(&_EQMutex);  // 解锁🔓
    sem_post(&Pop_Sem);//V(++)

}

Pop()

sem_wait(&Pop_Sem);通知Pop()占用情况。

cpp 复制代码
void Pop()
{
    sem_wait(&Pop_Sem);//P(--)
    pthread_mutex_lock(&mutex);

    std::cout << "消费了" << _BlockQueue.front() << std::endl;
    _BlockQueue.pop(val);
    
    pthread_mutex_unlock(&mutex);
    sem_post(&Equeue_Sem);  // V(++)生产者
}

信号量与条件变量的对比

固定已知大小的资源块,信号量进行维护,相对于条件变量的"循环判断(while(IsFull()))"信号量内部已经封装适时的阻塞与释放(另一方V(++)操作,所以信号量变量常大于等于2个)。

对比之下,条件变量用于了queue的"自动扩容机制",再使用信号量描述"容量",难以实现。

信号量:固定大小,块状形态。

条件变量:整体状态。

表示资源的特定信号量(像:有效数据、剩余空间)情况------"信号量是资源的一种预订机制"。

感谢支持,长期连载

欢迎关注

条件变量的封装

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

namespace CondModule
{
    class Cond
    {
        public:
        Cond()
        {}
        ~Cond() { perror("Cond Destroy() 未被调用!!\n"); }
        //条件变量
        //pthread_cond_init wait signal broadcast
        void Init()
        {
            int n = pthread_cond_init(&_cond, nullptr);
            if (!n) perror("Cond init failed\n");
        }
        void Wait(pthread_mutex_t* mutex)
        {
            int n = pthread_cond_wait(&_cond, mutex);
            if (!n) perror("Wait failed\n");
        }
        void Signal(pthread_cond_t* cond)
        {
            int n = pthread_cond_signal(cond);
            if (!n) perror("Signal failed\n");
        }
        void BroadCast(pthread_cond_t* cond)
        {
            int n = pthread_cond_broadcast(cond);
            if (!n) perror("Cond BroadCast failed\n");
        }
        void Destroy()
        {
            int n = pthread_cond_destroy(&_cond);
            if (!n) perror("Cond Destory failed\n");
        }

        private:
            pthread_cond_t _cond;
    };
};  // namespace CondModule

信号量的极简封装

cpp 复制代码
#pragma once
#include<semaphore.h>
//wait post
namespace SemModule
{
    class Sem
    {
        public:
        Sem(unsigned int val)
        {
            sem_init(&_sem,0,val);
        }
        ~Sem()
        {
            sem_destroy(&_sem);
        }
        void Wait()
        {
            //P(--)
            sem_wait(&_sem);
        }
        void Post()
        {
            sem_post(&_sem);
        }
        private:
            sem_t _sem;
    };
}