线程互斥+线程同步+生产消费模型

1. 线程互斥

1.1 互斥是什么

①每个线程内部访问全局资源的代码叫做临界区,呢没有访问全局资源的代码就是非临界区;

所以我们所有对全局资源的保护,就是对临界区代码的保护;

②被保护起来的资源是临界资源

1.2 互斥量/互斥琐mutex

ticketnum --不是原子的,也就是在第一个进程执行时会把ticketnum从内存中写到自己的CPU寄存器中(cpu寄存器的内容,叫做执行流(线程)的硬件上下文),对其进行--,然后将计算好的ticketnum写回内存,但这个时候发生线程切换,第二个线程再进来但是因为上一个线程没有把数据写进内存中,所以当前进车还会以为ticketnum的值是1000,这就是因为多线程并发切换导致的线程安全问题;

把数据从内存中搬到CPU内寄存器中,本质:共享->线程私有

1.2.1 什么是互斥锁

互斥量就是一把保护共享资源的锁 ,同一时间只允许一个线程持有它,就是访问当前的临界区代码,用来解决多线程同时读写同一块数据导致的混乱、错误问题。线程共享进程的内存空间,所以全局、静态、堆、成员变量都是共享数据;只有局部变量是线程私有。 这就是为什么必须用互斥锁------ 防止多个线程同时改同一块内存导致结果错乱。

1.2.2 怎么使用互斥琐

1.2.2.1 C++封住好的琐的调用接口

在实际开发中我们可以使用已经封装好的mutex类,它的调用函数是下面的这些;

① 头文件

cpp 复制代码
#include <mutex>  // 互斥锁
#include <thread>
#include <iostream>

② 创建琐

cpp 复制代码
std::mutex mtx;  // 全局 或 成员变量

③ 使用琐(读写共享数据时)

cpp 复制代码
// 共享数据(所有线程都能改)
int count = 0;

void thread_func() {
    // 要修改共享数据 → 必须加锁
    mtx.lock();       // 上锁
    count++;          // 安全修改
    mtx.unlock();     // 解锁
}

④ 更安全的方法(可以自动上锁和解锁)

cpp 复制代码
void thread_func() {
    std::lock_guard<std::mutex> lock(mtx); // 自动锁
    count++; // 安全操作
}
// 离开大括号自动解锁

A. std::mutex

真正的锁本体

底层是操作系统提供的同步原语

只有它能真正上锁、解锁

B. std::lock_guard<...>

一个工具类(锁守卫)

只干一件事:构造时自动 lock,析构时自动 unlock

作用:防止你忘记 unlock 导致死锁

C. lock

只是你给这个 lock_guard 对象起的变量名

叫 guard / l / mt 都行,不影响功能

1.2.2.2 琐的底层系统接口

但是我们在学习过程中也要学习底层琐的实现,下面的函数是是 POSIX 线程库(pthread) 的原生互斥锁接口,和前面C++ 封装 Mutex 类是「底层 vs 上层封装」的关系;这些是最底层的系统调用,直接和操作系统关联

初始化互斥量的两种方法:

cpp 复制代码
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER

这种方法是想在全局或者是静态定义一把琐,使用PRHREAD_mutex_INITIALIZER这样的宏定义

cpp 复制代码
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const
pthread_mutexattr_t *restrict attr);

定义一个局部的琐使用pthread_mutex_init;加锁的时候一定要保证细粒度,一定不能给大块代码进行加锁;

cpp 复制代码
// 销毁互斥锁(释放内核资源)
int pthread_mutex_destroy(pthread_mutex_t *mutex);

// 加锁:阻塞直到获取锁
int pthread_mutex_lock(pthread_mutex_t *mutex);

// 解锁:释放锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);

全局琐的测试:

cpp 复制代码
void Ticket()
{
   
    while(true)
    {
        pthread_mutex_lock(&lock); //加琐
        if(ticketnum >990)
        {
            usleep(1000);
            //usleep 函数可以让线程休眠指定时间,常用于模拟业务耗时、IO 延迟、处理时间、生产 / 消费
            //速度,让多线程并发效果更明显,方便测试线程安全与同步问题。
            std::cout << "get a new ticket,id: " << ticketnum<< std::endl;
            ticketnum--;
            pthread_mutex_unlock(&lock);
        }
        else
        {
            pthread_mutex_unlock(&lock);
            break;
        }
       
    }
}

int main()
{
    std::vector<std::thread> threads;
    for(int i=0;i<NUM;i++)
    {
        threads.emplace_back(Ticket);
        std::cout <<"thread "<<i <<" "<< "id"<<threads[i].get_id()<<std::endl;
    }
    for(auto & t :threads)
    {
        t.join();
    }
    std::cout <<"所有线程运行结束"<<std::endl;
    return 0;
}

每个线程先来申请琐,申请琐成功的线程才被允许访问临界区代码;被琐保护起来的共享资源称为临界资源,但是琐本身也是共享的,谁来保证琐的安全呢?

答案是在设计琐的时候就已经将加锁和解锁设置成了原子的;

我们该如何去看待这个琐呢?

看电影的时候都要抢票,这个时候我们需要信号量->也就是计数器,但是如果我们这个计数器是一的时候,这个资源是整体使用的,这就是二元信号量->就是琐;

如果申请琐的时候,已经被别的进程拿走了怎么办?

阻塞等待,直到解锁之后再去对琐进行竞争;

线程在访问临界区代码的时候,可不可以切换?

可以切换,时间片到了就可以且切换走;但是我被切换走之后,或者休眠之后,别的进程可以进来吗?不可以的,因为我还是会将琐带着,这就是为什么加琐之后效率降低,所以在我访问的时候在别的线程看来只有访问前和访问后这两种对我来说有用,所以在别的线程看来就是原子的了;

所以一旦明白这个资源是共享的,并且可能被多线程并发访问,这部分资源就必须使用琐来保护;

局部琐的测试

cpp 复制代码
int main()
{
    pthread_mutex_t lock;
    pthread_mutex_init(&lock,nullptr);

    std::vector<std::thread> threads;
    for(int i=0;i<NUM;i++)
    {
        threads.emplace_back(Ticket,&i);
        std::cout <<"thread "<<i <<" "<< "id"<<threads[i].get_id()<<std::endl;
    }
    
    for(auto & t :threads)
    {
        t.join();
    }
    std::cout <<"所有线程运行结束"<<std::endl;
    pthread_mutex_destroy(&lock);
    return 0;
}

销毁互斥锁的注意事项:

①使用 PTHREAD_MUTEX_INITIALIZER 初始化的互斥量不需要销毁

因为这种是静态初始化(静态分配),锁的资源由系统自动管理,存在进程的静态存储区

它没有动态申请内核资源,所以不需要手动释放;如果你强行 pthread_mutex_destroy,会导致未定义行为(程序崩溃)。

②不要销毁一个已经加锁的互斥量锁

互斥量琐正在被占用,代表有线程正在使用它保护的资源,这个时候你直接销毁,相当于把正在用的锁强行拆掉,造成的后果就是,其他线程会永远阻塞(死锁),进程直接崩溃,资源泄漏;之所以不能销毁一个已经加锁的互斥量琐是因为销毁之后线程的等待队列会被销毁,琐里面的资源也会被释放,但是阻塞的线程不会主动检查琐,它们是在等待队列中休眠,只有等上一个线程被正常的unlock()之后,操作系统才会主动唤醒等待的线程,把琐标记为未锁定,然后再等待队列中叫醒一个线程,告诉它:"你该拿琐了!"而直接销毁的线程没有走unlock的正常流程,没有OS叫醒它,它就会永远睡下去,也就是永久阻塞;

③已经销毁的互斥量,要确保后面不会有线程再尝试加锁

因为琐被销毁后,锁的内核资源已经被释放,这个 mutex 现在是野变量、无效变量,如果再对它加锁 / 解锁 = 操作野指针,后果就是:程序崩溃、内存错误、线程卡死

调用 pthread_mutex_lock 时,可能会遇到以下情况:

①若互斥量处于未锁定状态,该函数会将互斥量锁定,然后返回成功。

②若调用时,其他线程已经锁定了互斥量,则当前线程会陷入阻塞(执行流被挂起,挂起是指CPU不再调度),直到互斥量被其他线程解锁,当前线程才会被唤醒并重新尝试加锁。

③若多个线程同时竞争互斥量,只有一个线程能成功锁定互斥量,其余未竞争成功的线程都会被阻塞,等待互斥量释放。

1.2.3互斥量加锁的基本原理

大多数的体系结构中都提供了swap和exchange指令,体系机构就是硬件,现在这个指令把寄存器和内存中的数据进行交换只使用了一条语句;而我们的琐跟他们的关系是什么呢?琐就是内存中的一块数据,当第一个线程想要申请琐的时候,exchange语句将琐数据和寄存器数据进行交换,之后将内存上琐位置标记为0,将al寄存器的内容标记为1,当这个有琐的线程进行图片上的if判断语句时,可以就会return 0,也就是访问临界区代码,下一个线程申请琐的时候也是先进行exchange语句,这个时候因为上一个进程已经把1拿走了,寄存器和内存交换了还是0,所以if判断不成功,当前线程只能鼓挂起等待了;

1.2.5死锁

在琐上申请琐,就是死锁;

1.2.4 用C++类封装互斥锁

cpp 复制代码
Mutex(const Mutex &) = delete;

这句代码是 C++11 里的禁用拷贝构造函数 ,专门用来禁止你的锁对象被复制、被拷贝;

为什么要禁止复制锁?

因为 锁(Mutex)是独一无二的,不能有副本!想象一下:一把锁 = 保护一段数据,如果你复制了这把锁

就会出现两把锁保护同一个东西,结果就是线程安全直接崩溃、死锁、数据错乱,所以所有正规的锁、 muduo 库里的锁、标准库 std::mutex全都禁止拷贝

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

namespace LockModule
{
    class Mutex
    {
    public:
        Mutex(const Mutex&)= delete;
        const Mutex& operator = (const Mutex&) =delete;
        Mutex()
        {
            int n  =:: pthread_mutex_init(&_lock,nullptr);
            (void) n;
        }
        ~Mutex()
        {
            int n = ::pthread_mutex_destroy(&_lock);
            (void) n;
        }
        void Lock()
        {
            int n =::pthread_mutex_lock(&_lock);
            (void) n;
        }
        void UnLock()
        {
             int n =::pthread_mutex_unlock(&_lock);
            (void) n;
        }

    private:
        pthread_mutex_t _lock;
    };

    class Lockguard
    {
        public:
        Lockguard(Mutex &mtx)
        :_mtx(mtx)
        {
           _mtx.Lock();
        }
        ~Lockguard()
        {
            _mtx.UnLock();
        }

        private:
         
        Mutex &_mtx;

    };

}

2.线程同步

保证安全的前提下,让系统变得合理高效;回忆超级自习室的故事或者拿苹果问题,即不能让一个线程进入临界资源之后什么也不做,这样会让别的线程产生"饥饿问题"释放完琐之后又立即申请琐,一直这样就会造成资源的浪费,所以需要有对应规则,线程在释放完琐之后应该归还琐,然后再去进程队列中排队;这就要用到条件变量了;

2.1条件变量

2.1.1条件变量是什么

条件变量是多线程编程中用于实现线程间高效同步的核心机制,主要解决互斥锁无法处理的 "线程等待特定条件成立" 的问题 ,当一个线程借助互斥锁安全地访问共享变量、共享队列等数据时,即便成功获取了锁,也可能发现当前的共享状态无法让自己继续执行任务,在其他线程修改共享状态之前,该线程完全无法开展有效工作,此时如果一直占用锁进行循环判断,会造成 CPU 资源的严重浪费,而条件变量正是为这类场景设计的

最典型的例子就是生产者 - 消费者模型,消费者线程在持有互斥锁访问任务队列时,一旦发现队列为空,就没有任务可以处理,只能暂停运行并等待,直到生产者线程向队列中添加新的节点或任务,改变了共享队列的状态后,消费者线程才能被唤醒并继续工作,这种需要线程主动等待、在条件满足时被其他线程精准唤醒,同时避免忙等导致性能损耗的同步需求,无法仅依靠互斥锁实现,因此必须使用条件变量来完成线程的阻塞等待与唤醒通知,保证多线程协作的高效性与安全性。

2.1.2条件变量怎么使用

①条件变量函数

A.初始化

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

参数:

cond:要初始化的条件变量

attr:NULL

B.销毁

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

C.等待条件满足

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

参数:

首先在使用这个函数的时候必须保证当前进程拿着琐,POSIX标准明确规定,调用pthread_cond_wait时,调用线程必须已经对mutex加锁,第一个参数是线程告诉函数你要在哪个条件上等待,如果队列空了、任务没到、数据没准备好,线程就会阻塞在这个条件上,直到别人调用pthread_cond_signal唤醒它,第二个参数mutex是你已经加锁成功的琐,函数会把这个琐自动释放,然后线程进入睡眠,当线程被唤醒时,函数会自动把琐重新要回来;

我现在拿着锁,但是条件不满足,我要睡觉了。请帮我把锁释放,让别人能用;等我被唤醒时,再自动把锁还给我;

D.唤醒等待

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

2.2 基于条件变量的demo

我们这个demo要实现的是让主线程控制新线程,即我想让它等待它就等待,想让它执行就执行;

cpp 复制代码
#include <iostream>
#include <thread>
#include <vector>
#include <unistd.h>
#include <cstdio>

pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond=PTHREAD_COND_INITIALIZER;

void * routine(void * args)
{
    std::string name = static_cast<const char *> (args);
   
    while(true)
    {
       pthread_mutex_lock(&mutex);
       pthread_cond_wait(&cond,&mutex);
       //printf("im wait.im %s\n",name.c_str());
       printf("%s is a routine!\n",name.c_str());
       pthread_mutex_unlock(&mutex);
    }
    
    return nullptr;

}

int main()
{
    pthread_t tid1,tid2,tid3;
    pthread_create(&tid1,nullptr,routine,(void*)"thread -1");
    pthread_create(&tid2,nullptr,routine,(void*)"thread -2");
    pthread_create(&tid3,nullptr,routine,(void*)"thread -3");

    while(true)
    {
       //唤醒一个线程
    //    pthread_cond_signal(&cond);
    //    printf("main thread wakeup one thread!\n");
    //    sleep(3);
       //唤醒多个线程
       printf("wakeup many threads!\n");
       pthread_cond_broadcast(&cond);
       sleep(1);

    }

    pthread_join(tid1,nullptr);
    pthread_join(tid2,nullptr);
    pthread_join(tid3,nullptr);
    return 0;
}

这是主线程一次唤醒一个线程;

主线程一次唤醒多个线程

3.生产者消费者模型

321原则->便于记忆

3.1什么是生产者-消费者模型

我们去在超市中买东西,超市的东西是供应商,每次超市都会买很多的东西给我们卖,我们直接去超时买东西会比直接去工厂方便很多,而超市在我们看来其实就是缓存;对工厂进行更新和替代跟消费者没有关系,而消费者的变化跟工厂也没有关系,这就将消费者和生产者解耦了,并且因为有超市,可以多进货、少进货,这就能做到忙闲不均;

生产者消费者模型是多线程编程中非常经典的协同工作模式,简单来说,就是存在两类线程和一个共享的缓冲区,其中"生产者线程"负责不断产生数据、任务或资源,并将其放入缓冲区中,而"消费者线程"则负责不断从缓冲区中取出数据、任务或资源进行处理,缓冲区作为两者之间的中间容器;

既可以让生产者和消费者解耦,不需要直接相互依赖,也能平衡两者的处理速度差异,避免因为一方过快、另一方过慢而导致资源浪费或数据混乱;在实际运行中**,如果缓冲区已满,生产者就需要暂停生产并等待,直到消费者取出数据腾出空间,如果缓冲区为空,消费者就需要暂停消费并等待,直到生产者放入新的数据**,这种等待与唤醒的配合通常需要借助互斥锁保证缓冲区的安全访问,再通过条件变量实现高效的线程同步,从而让生产者和消费者能够有序、稳定、高效地协同工作,是实现高并发、任务队列、异步处理等场景最常用的设计模式。

3.2生产者消费者模型的作用

在多线程和高并发开发中,几乎所有需要异步处理、任务排队、线程协作的场景都会使用生产者消费者模型,因为它能完美解决处理速度不匹配、线程安全、资源浪费、耦合过强 这几大核心问题。比如一个线程负责产生任务或数据(生产者),另一个线程负责执行任务或处理数据(消费者),中间用一个队列作为缓冲区,生产者只管往队列里放,消费者只管从队列里取,两者互不直接依赖,实现了解耦当生产者生产过快时,任务会在队列中排队,不会直接压垮消费者,当消费者处理过快而队列为空时,线程可以通过条件变量休眠等待,避免 CPU 空转浪费资源,同时依靠互斥锁保证队列在多线程访问时的数据安全。无论是网络服务器的请求处理、消息队列、线程池任务调度、日志异步输出,还是操作系统的 IO 处理,本质上都是生产者消费者模型的具体应用,它结构清晰、稳定高效,能让多线程之间有序协作,因此成为了并发编程中最基础、最通用、几乎无处不在的设计模式。

3.3 生产者消费者模型的使用场景

① 管道

匿名管道(pipe)或命名管道(FIFO)本质上就是操作系统提供的一种基于内核缓冲区的进程间通信机制,完全符合生产者消费者模型的结构与思想。管道的一端用于写入数据,扮演生产者的角色,负责向内核缓冲区 "生产" 并放入数据;另一端用于读取数据,扮演消费者的角色 ,负责从内核缓冲区 "消费" 并取出数据。当管道缓冲区为空时,读进程会阻塞等待,直到有进程写入数据;当管道缓冲区写满时,写进程会阻塞等待,直到有进程读出数据腾出空间。这种一方生产、一方消费、依靠缓冲区解耦、空则等待、满则阻塞的工作方式,与生产者消费者模型的设计思想完全一致,因此管道可以看作是操作系统层面原生实现的生产者消费者模型。

②消息队列

消息队列是操作系统提供的进程间通信机制,其内部完全遵循生产者消费者模型的设计思想。发送消息的进程或线程作为生产者,通过 msgsnd 函数将数据放入内核维护的消息队列缓冲区中;接收消息的进程或线程作为消费者,通过 msgrcv 函数从缓冲区中取出数据进行处理。当缓冲区为空时,消费者会阻塞等待,直到有新的消息到来;当缓冲区满时,生产者会阻塞等待,直到有消息被取出腾出空间。消息队列由内核自动完成并发安全控制与同步唤醒,不需要用户手动实现互斥锁和条件变量,是一种开箱即用的生产者消费者模型实现。

③ 其他

线程池中的任务队列 同样是生产者消费者模型,主线程或 IO 线程往队列里提交任务作为生产者,工作线程取出任务执行作为消费者,空队列时线程等待,有任务时被唤醒;还有环形缓冲区 ,通常用数组实现,配合互斥锁和条件变量,在嵌入式、高性能网络编程中非常常见;此外,消息中间件如 Kafka、Redis 队列、MQ 等,本质上也是分布式环境下的生产者消费者模型,发送方生产消息,消费方处理消息,依靠中间件缓存实现削峰、异步和解耦。这些实现虽然底层机制不同,但都遵循 "一方生产、一方消费、缓冲区解耦、同步等待" 的核心思想。

3.4生产者消费者模型的优点

生产者消费者模型通过生产者、缓冲区、消费者三者分离的结构,在多线程和多进程并发场景中带来了非常明显的优势。

首先它实现了解耦 ,生产者和消费者不需要直接交互,甚至不需要知道对方的存在,只依赖中间缓冲区通信,让程序结构更清晰、更容易维护和扩展。其次它能平衡速度差异,当生产者生产速度快于消费者处理速度时,缓冲区可以暂存多余数据,避免数据丢失或直接压垮系统;当消费者处理更快时,也可以在有数据时立即处理,不会空等。

同时它有效避免了CPU 空转浪费 ,借助条件变量或阻塞机制,让消费者在缓冲区为空时休眠等待、生产者在缓冲区满时休眠等待,而不是一直循环判断占用 CPU 资源,大幅提升系统效率。此外,该模型还能保证数据安全与有序执行,配合互斥锁可以避免多线程同时操作共享数据带来的竞争问题,让生产和消费按合理顺序进行,保证数据一致性。

最后它支持异步处理与削峰填谷,在高并发场景下可以缓冲突发大量任务,避免瞬间压力导致系统崩溃,广泛适用于线程池、消息队列、网络服务、日志系统等场景,稳定性和通用性极强。

3.5 C++ 模拟阻塞队列的生产消费模型

3.5.1什么是阻塞队列

在多线程编程中阻塞队列是一种常用于生产者和消费者模型的数据结构,其与普通队列区别在于,当队列为空时,从队列获取元素的操作符会被阻塞,直到队列中被放入了元素,当队列为满时,往队列放元素的操作也会被阻塞,直到有元素从队列中取出

3.5.2 单生产者和单消费模型实现

main.cc

cpp 复制代码
#include "BlockQueue.hpp"
using namespace BlockQueueModule;

void *Consumer(void * args)
{
    BlockQueue<int> * bq = static_cast<BlockQueue<int>*> (args);
    while(true)
    {
        sleep(2);
        int data;
        //1.从bq中拿数据
        bq->Pop(&data);
        //2.做处理
        printf("consumer ,get a data:%d\n",data);
         
    }

}


void * Productor(void * args)
{
    BlockQueue<int>* bq = static_cast<BlockQueue<int>*>(args);
    int data=10;
    while(true)
    {
        //1.从外部获取数据
        //2.生产到bq中
        bq->Equeue(data);
        printf("productor ,create a data:%d\n",data);
        data++;
    }
}


int main()
{
   BlockQueue<int> * bq = new BlockQueue<int>(5);
   pthread_t c,p;
   pthread_create(&c,nullptr,Consumer,bq);
   pthread_create(&p,nullptr,Productor,bq);

   pthread_join(c,nullptr);
   pthread_join(p,nullptr);

   delete bq;
   return 0;
}

BlockQueue.hpp

cpp 复制代码
#pragma once

#include <iostream>
#include <queue>
#include <unistd.h>
#include <pthread.h>

namespace BlockQueueModule
{
    static const int gcap = 10;
    template <typename T>
    class BlockQueue
    {
    public:
        bool IsFull() { return _cap == _q.size(); }
        bool IsEmpty() { return _q.empty(); }

        BlockQueue(int cap = gcap)
            : _cap(cap), _cwait_num(0), _pwait_num(0)
        {
            pthread_mutex_init(&_mutex, nullptr);
            pthread_cond_init(&_productor_cond, nullptr);
            pthread_cond_init(&_consumer_cond, nullptr);
        }
        void Equeue(const T &in)
        {
        
            pthread_mutex_lock(&_mutex);
            if (IsFull())
            {
                 _pwait_num++;
                std::cout << "生产者进入等待..." << std::endl;
                pthread_cond_wait(&_productor_cond, &_mutex);
                _pwait_num--;
                std::cout << "生产者被唤醒..." << std::endl;
            }
                _q.push(in);
                _pwait_num++;
                if (_pwait_num)
                {
                    //std::cout << "消费者被唤醒..." << std::endl;
                    pthread_cond_signal(&_consumer_cond);
                    _pwait_num--;
                }
                pthread_mutex_unlock(&_mutex);
        
        }
        void Pop(T *out)
        {
            pthread_mutex_lock(&_mutex);
            if (IsEmpty())
            {
                 _cwait_num++;
                std::cout << "消费者进入等待..." << std::endl;
                pthread_cond_wait(&_consumer_cond, &_mutex);
                _cwait_num--;
                std::cout << "消费者被唤醒..." << std::endl;
                
            }
                *out = _q.front();
                std::cout << "消费者被唤醒..." << std::endl;
                _q.pop();
                _cwait_num++;
                if (_cwait_num)
                {
                    pthread_cond_signal(&_productor_cond);
                    _cwait_num--;
                }
            
                pthread_mutex_unlock(&_mutex);

        }
        ~BlockQueue()
        {
            pthread_mutex_destroy(&_mutex);
            pthread_cond_destroy(&_productor_cond);
            pthread_cond_destroy(&_consumer_cond);
        }

    private:
        std::queue<T> _q;
        int _cap;
        pthread_mutex_t _mutex;
        pthread_cond_t _productor_cond;
        pthread_cond_t _consumer_cond;
        int _cwait_num;
        int _pwait_num;
    };
}

唤醒是既可以放在unlock之前,也可以放在unlock外面,目标线程被唤醒不再条件变量上等琐,而是在琐上等,而我快,我放掉琐,对方就能立马申请了;在unlock外被唤醒的话,再多消费、生产者模型中,如果这个时候的生产者没有拿上琐,被别的线程拿走了,这个时候这个生产者线程会再回到pthread_cond_wait()的琐上等待;

所以我们的结论就是如果有多个生产者消费者的模型中,最好先唤醒再unlock;

在条件没有满足的时候就被唤醒,就比如说使用bradcast很多唤醒,就可能发生;所以我们在使用pthread_cond_wait()使用的不是if而是while,因为while会再判断一次;

多生产者多消费者模型:因为我们今天使用的是一个琐,所以消费者和消费者,生产者和生产者之间天然就是互斥的;

相关推荐
Albert Edison2 小时前
【ProtoBuf 语法详解】更新消息|保留字段|未知字段
开发语言·c++·protobuf
feifeigo1232 小时前
近场声全息(NAH)数据与MATLAB实现
开发语言·matlab
李彦亮老师(本人)2 小时前
Rocky Linux 9.x 安全加固实战指南:从系统初始化到生产级防护
linux·运维·安全·rocky
RisunJan2 小时前
Linux命令-mount(用于挂载Linux系统外的文件)
linux·运维·服务器
fie88892 小时前
基于MATLAB的非线性模型预测控制(NMPC)在CSRT系统中的应用
开发语言·matlab
⑩-2 小时前
Java基础+集合框架-八股文
java·开发语言
福运常在2 小时前
股票数据API(19)次新股池数据
java·python·maven
Zaki_gd2 小时前
Cortex-M7 D-Cache 与 DMA 缓存一致性说明
java·spring·缓存
多看书少吃饭2 小时前
Vue3 + Java + Python 打造企业级大模型知识库(含 SSE 流式对话完整源码)
java·python·状态模式