【Linux】线程的同步与互斥

目录

[1. 整体学习思维导图](#1. 整体学习思维导图)

[2. 线程的互斥](#2. 线程的互斥)

[2.1 互斥的概念](#2.1 互斥的概念)

[2.2 见一见数据不一致的情况](#2.2 见一见数据不一致的情况)

[2.3 引入锁Mutex(互斥锁/互斥量)](#2.3 引入锁Mutex(互斥锁/互斥量))

[2.3.1 接口认识](#2.3.1 接口认识)

[2.3.2 Mutex锁的理解](#2.3.2 Mutex锁的理解)

[2.3.3 互斥量的封装](#2.3.3 互斥量的封装)

[3. 线程同步](#3. 线程同步)

[3.1 条件变量概念](#3.1 条件变量概念)

[3.2 引入条件变量Cond](#3.2 引入条件变量Cond)

[3.2.1 接口认识](#3.2.1 接口认识)

[3.2.2 同步的封装](#3.2.2 同步的封装)

[3.2.3 为什么 pthread_cond_wait 需要互斥量?](#3.2.3 为什么 pthread_cond_wait 需要互斥量?)

[4. 生产者/消费者模型](#4. 生产者/消费者模型)

[4.1 基于BlockingQueue的生产者消费者模型](#4.1 基于BlockingQueue的生产者消费者模型)

[4.3 封装实现生产者/消费者模型](#4.3 封装实现生产者/消费者模型)


1. 整体学习思维导图

2. 线程的互斥

2.1 互斥的概念

我们前面已经在进程通信信号量的时候简单了解过了互斥概念,以下是重要概念的回顾:

  1. **临界资源:**多线程执行流共享的资源就叫做临界资源

  2. **临界区:**每个线程内部,访问临界资源的代码,就叫做临界区

  3. 互斥 **:**任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用,保护临界区代码就是变相保护临界资源

  4. **原子性:**不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成

2.2 见一见数据不一致的情况

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

/* 抢票系统 */
int ticket = 1000; /* 票数 */
void *routine(void *args)
{
    std::string name = static_cast<const char*>(args);
    while(true)
    {
        if(ticket > 0)
        {
            usleep(1000);
            std::cout << name << " Get a ticket, having: " << ticket << " leave" << std::endl;
            ticket--;
        }
        else
        {
            break;
        }
    }
    return nullptr;
}

int main()
{
    pthread_t t1, t2, t3, t4;
    pthread_create(&t1, nullptr, routine, (void*)"thread-1");
    pthread_create(&t2, nullptr, routine, (void*)"thread-2");
    pthread_create(&t3, nullptr, routine, (void*)"thread-3");
    pthread_create(&t4, nullptr, routine, (void*)"thread-4");

    pthread_join(t1, nullptr);
    pthread_join(t2, nullptr);
    pthread_join(t3, nullptr);
    pthread_join(t4, nullptr);
    return 0;
}

我们会发现到最后票已经变成负数了,这就是全局变量资源共享会带来的问题,这个问题具体是怎么发生的呢?

  1. 为什么数据会不一致?

1\] `ticker-- `这个操作不是原子的,可能会出现多个线程同时访问执行这条语句,我们将`ticket-- `转换成底层的汇编: ```cpp 0XFF00: 载入 ebx ticket 0XFF02:减少 ebx 1 0XFF04: 写回 0X1111 ebx ``` 我们将这三步放到CPU访问内存的操作来看: ![](https://i-blog.csdnimg.cn/direct/5c4e53f5dd464dc89ee10b4bf03ebf97.png) 结合场景:现在有一个线程A,执行完第二步 `ebx:99 pc:0XFF04` 时,刚好调度时间片到了,保存上下文数据切换到下一个线程。线程B切换上来将100加载进行--,直到最后ticket--到了1,此时线程B的调度时间片到了,保存上下文数据,`ebx:1 pc:0XFF02`。恢复线程A的调度,执行pc执行,ticket又再次被写回到99,这就是数据不一致问题! 从以上我们可以得知,我们认为只有一条汇编语句的就是原子的,多条汇编语句会带来线程切换问题,一旦切换就会导致数据不是原子的! **为什么我们的程序ticket最后值为负数?** ticket作为全局变量,当一些线程执行`ticket > 0`逻辑计算之后,正好被切走,正好此时`ticket == 1`。然后又来了一个线程进入进行判断`ticket--`到`0`,然后之前被切走的线程又进来--就导致ticket为负数了! 由此我们可以得出一个结论,对于全局资源的访问我们需要加保护,一次只让一个线程进行访问资源,多线程下可能会出现并发问题,这种问题我们叫做线程安全问题! ### 2.3 引入锁Mutex(互斥锁/互斥量) #### 2.3.1 接口认识 锁的创建: ```cpp ① ⽅法1,静态分配: pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER ② ⽅法2,动态分配: int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr); 参数: mutex:要初始化的互斥量 attr:nullptr ``` 锁的定义有两种一种是全局性的锁,一种是局部的锁: * 如果定义的是全局或者静态的锁,可以只使用pthread_mutex_t 锁的名字=PTHREAD_MUTEX_INITIALIZER * 如果定义的这把锁是动态申请的,比如new或栈上开辟的,必须使用pthread_mutex_init函数来进行初始化。参数1就是你自己定义的锁,参数2是属性,直接设为nullptr即可。 锁的销毁: ```cpp int pthread_mutex_destroy(pthread_mutex_t *mutex); 参数: mutex:要销毁的互斥量 ``` 销毁互斥量需要注意: * 使用 `PTHREAD_MUTEX_INITIALIZER` 初始化的互斥量不需要销毁,因为此时锁是静态或全局的,不需要 destroy,全局的或者静态的变量会随着进程的运行而一直存在,进程结束他也就自动释放了 * 不要销毁⼀个已经加锁的互斥量 * 已经销毁的互斥量,要确保后面不会有线程再尝试加锁 互斥量的加锁和解锁: ```cpp int pthread_mutex_lock(pthread_mutex_t *mutex); int pthread_mutex_unlock(pthread_mutex_t *mutex); 返回值: 成功返回0,失败返回错误号 参数: mutex:要销毁的互斥量 ``` 调用 pthread_ lock 时,可能会遇到以下情况: * 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功。 * 发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么 `pthread_lock` 调用会陷入阻塞(执行流被挂起),等待互斥量解锁(多线程并发竞争锁访问临界资源)。 代码修改,了解锁之后我们可以通过修改代码加锁保证抢票不会到达负数: 全局锁: ```cpp #include #include #include #include /* 抢票系统 */ int ticket = 1000; /* 票数 */ /* 全局锁 */ pthread_mutex_t glock = PTHREAD_MUTEX_INITIALIZER; void *routine(void *args) { std::string name = static_cast(args); while(true) { /* 加锁 */ pthread_mutex_lock(&glock); if(ticket > 0) { usleep(1000); std::cout << name << " Get a ticket, having: " << ticket << " leave" << std::endl; ticket--; /* 解锁 */ pthread_mutex_unlock(&glock); } else { /* 解锁 */ pthread_mutex_unlock(&glock); break; } } return nullptr; } int main() { pthread_t t1, t2, t3, t4; pthread_create(&t1, nullptr, routine, (void*)"thread-1"); pthread_create(&t2, nullptr, routine, (void*)"thread-2"); pthread_create(&t3, nullptr, routine, (void*)"thread-3"); pthread_create(&t4, nullptr, routine, (void*)"thread-4"); pthread_join(t1, nullptr); pthread_join(t2, nullptr); pthread_join(t3, nullptr); pthread_join(t4, nullptr); return 0; } ``` 局部锁: ```cpp /* 抢票系统 */ int ticket = 1000; /* 票数 */ /* 局部锁 */ pthread_mutex_t lock; void *routine(void *args) { std::string name = static_cast(args); pthread_mutex_init(&lock, nullptr); while(true) { /* 加锁 */ pthread_mutex_lock(&lock); if(ticket > 0) { usleep(1000); std::cout << name << " Get a ticket, having: " << ticket << " leave" << std::endl; ticket--; /* 解锁 */ pthread_mutex_unlock(&lock); } else { /* 解锁 */ pthread_mutex_unlock(&lock); break; } } return nullptr; } int main() { pthread_t t1, t2, t3, t4; /* 初始化 */ pthread_mutex_init(&lock, nullptr); pthread_create(&t1, nullptr, routine, (void*)"thread-1"); pthread_create(&t2, nullptr, routine, (void*)"thread-2"); pthread_create(&t3, nullptr, routine, (void*)"thread-3"); pthread_create(&t4, nullptr, routine, (void*)"thread-4"); pthread_join(t1, nullptr); pthread_join(t2, nullptr); pthread_join(t3, nullptr); pthread_join(t4, nullptr); /* 销毁 */ pthread_mutex_destroy(&lock); return 0; } ``` ![](https://i-blog.csdnimg.cn/direct/a1b6bc80d4c941cea49691dd7b0a133a.png) **总结:** * 竞争申请锁,所有线程看到锁,说明锁也是临界资源,因此申请锁的过程必须是原子的!申请锁成功继续向后运行,访问临界区代码,访问临界区资源。失败,阻塞挂起申请的执行流。 * 锁的本质:让执行临界区的代码的执行流由并行变成串行! *** ** * ** *** 1. 所以的线程都必须遵守锁的竞争规则,不可以有线程无视锁。 2. 加锁之后,临界区依然可以进行线程切换,但是没有解锁之前,其他线程无法访问临界区,只有当持有锁的线程释放锁之后,线程们再次展开对锁的竞争,访问临界区。 #### 2.3.2 Mutex锁的理解 **锁有两种实现方式:** 1. 硬件实现: 操作系统会收到时钟中断去检查线程的时间片是否被调度完全,如果完了就进行切换操作,我们加锁不就是让一个线程不做切换吗,因此我们可以让操作系统忽略收到的时钟中断即可!(只做了解) 2. 软件实现: 软件实现的过程是通过CPU的%al寄存器,执行`swap/change`操作完成加锁的过程。 理解软件实现的加锁过程: \[1\] CPU的上下文 我们知道CPU在调度一个线程时,会将其上下文数据加载到寄存器,调度结束时也会保存上下文数据。 \[2\] 竞争锁 现在内存中一块锁的区域,默认值为1,CPU调度线程竞争锁时,会先将CPU寄存器%al的内容置为0,去对内存中的lock做一个交换! ![](https://i-blog.csdnimg.cn/direct/25bb94f161944bd78ef30d2df2becba7.png) [我们发现锁的申请过程只有一条汇编语句,这与我们前面理解原子概念的猜想是吻合的!因此申请锁是原子的](https://g1pic5l6443.feishu.cn/wiki/FZC7weDLMi0tBnk9IJ8ceWHWnSg#share-SYhzdOYvUoJP6XxT8QJcqqN8nwb "我们发现锁的申请过程只有一条汇编语句,这与我们前面理解原子概念的猜想是吻合的!因此申请锁是原子的") #### 2.3.3 互斥量的封装 我们这样使用锁是否有点太不方便了,我们可不可以将锁封装成一个对象,进行使用呢? **Mutex.hpp** ```cpp #pragma once #include namespace mutexdouble { class Mutex { public: Mutex() { pthread_mutex_init(&_mutex, nullptr); } void Lock() { pthread_mutex_lock(&_mutex); } void Unlock() { pthread_mutex_unlock(&_mutex); } ~Mutex() { pthread_mutex_destroy(&_mutex); } private: pthread_mutex_t _mutex; }; /* RAII风格 */ class MutexGuard { public: MutexGuard(Mutex &lock) :_lock(lock) { _lock.Lock(); } ~MutexGuard() { _lock.Unlock(); } private: Mutex &_lock; /* 注意需要是引用 */ }; } ``` **测试:** ```cpp /* 抢票系统 */ int ticket = 1000; /* 票数 */ Mutex lock; void *routine(void *args) { std::string name = static_cast(args); while (true) { { MutexGuard guard(lock); if (ticket > 0) { usleep(1000); std::cout << name << " Get a ticket, having: " << ticket << " leave" << std::endl; ticket--; } else { break; } } } return nullptr; } int main() { pthread_t t1, t2, t3, t4; pthread_create(&t1, nullptr, routine, (void *)"thread-1"); pthread_create(&t2, nullptr, routine, (void *)"thread-2"); pthread_create(&t3, nullptr, routine, (void *)"thread-3"); pthread_create(&t4, nullptr, routine, (void *)"thread-4"); pthread_join(t1, nullptr); pthread_join(t2, nullptr); pthread_join(t3, nullptr); pthread_join(t4, nullptr); return 0; } ``` ![](https://i-blog.csdnimg.cn/direct/d576a793b11e4b7ea2c3010d4759d69a.png) * C++内部也已经封装了mutex互斥对象,头文件为 `#include ` ```cpp /* RAII 风格 */ std::mutex mtx; std::lock_guard guard(mtx); ``` ## 3. 线程同步 * 前面我们知道互斥是为了解决\[`数据不一致/多线程由并行访问临界资源到串行访问`\]的问题,但是试想一下多个线程抢夺互斥锁,如果其中一个线程多次连续抢到锁,其他线程一直没法访问临界资源会带来什么问题?**线程饥饿** ![](https://i-blog.csdnimg.cn/direct/935bb6d09015436fb3555310c4556cf4.png) * 那么怎么解决呢?同步的诞生就是问题解决互斥锁带来的问题,有没有一种方式使得线程们可以有着平均拿到锁的机会?有顺序的访问?这就是同步需要解决的!也是同步的作用! ### 3.1 条件变量概念 我们知道线程在没有竞争到锁之后会被阻塞挂起,我们现在需要让每个线程都有获取锁的机会,第一个问题是怎么唤醒被挂起的线程;第二个问题我们需要让已经使用过锁的线程暂缓一会再获得锁。 * 为了解决第一个问题我们需要引入新的概念:条件变量 ```bash 场景: 角色:放苹果的人,等待获取苹果的线程队列 中间区域:一个盘子 提示:铃铛 放苹果和取苹果的过程是互斥的需要加上锁,当放苹果的人在盘子放一个苹果,此时他就会摇铃铛告诉队列的线程,队列front的线程取苹果,然后再次排队到线程队列的最后。至此互斥和同步互补解决了多线程访问的数据不一致和线程饥饿问题!因此线程在对应的条件变量下等待,此处的条件变量是铃铛和队列 ``` ![](https://i-blog.csdnimg.cn/direct/1861a5554c364860a847ad4475e5d960.png) ### 3.2 引入条件变量Cond #### 3.2.1 接口认识 1. 初始化和销毁 ```cpp 初始化: int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr); 参数: cond:要初始化的条件变量 attr:NULL 销毁: int pthread_cond_destroy(pthread_cond_t *cond) pthread_cond_t cond = PTHREAD_COND_INITIALIZER ``` > 条件变量是 pthread_cond_t 的数据类型。它的使用跟前面互斥锁一样,可以定义成局部或者全局的。 > > 如果是全局或者静态的,可以直接使用 PTHREAD_COND_INITIALIZER 初始化。 > > 如果是局部的,就用pthread_cond_init 函数初始化,使用完了就destroy销毁掉 2. 等待条件 ```cpp int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex); 参数: cond:条件变量,线程将在这个条件变量上等待。 mutex:互斥锁,与条件变量关联的互斥锁 作用:让调用线程在条件变量(cond)上等待,同时释放与之关联的互斥锁(mutex), 并在被唤醒后重新获取互斥锁。 ``` > 线程条件不满足时,线程就要等待,要在指定的条件变量上等待 3. 唤醒等待 ```cpp int pthread_cond_broadcast(pthread_cond_t *cond); int pthread_cond_signal(pthread_cond_t *cond); ``` > 等待完成后,就要进行唤醒。 > > **pthread_cond_signal** 表示唤醒一个线程,**pthread_cond_broadcast**表示唤醒所有线程。 **测试:** ```cpp /* 条件变量 */ pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t cond = PTHREAD_COND_INITIALIZER; void *active(void *arg) { std::string name = static_cast(arg); while(true) { pthread_mutex_lock(&mutex); // 没有对于资源是否就绪的判定 pthread_cond_wait(&cond, &mutex); printf("%s is active!\n", name.c_str()); pthread_mutex_unlock(&mutex); } } int main() { pthread_t tid1, tid2, tid3; pthread_create(&tid1, nullptr, active, (void*)"thread-1"); pthread_create(&tid2, nullptr, active, (void*)"thread-2"); pthread_create(&tid3, nullptr, active, (void*)"thread-3"); sleep(1); printf("Main thread ctrl begin...\n"); while(true){ printf("main wakeup thread...\n"); pthread_cond_signal(&cond); // 一个一个唤醒 // pthread_cond_broadcast(&cond); // 全部唤醒 sleep(1); } pthread_join(tid1, nullptr); pthread_join(tid2, nullptr); pthread_join(tid3, nullptr); return 0; } ``` ![](https://i-blog.csdnimg.cn/direct/be3525b784c94eebb5de4ada98c537dd.png) ```cpp pthread_cond_wait(&cond, &mutex); ``` 为什么等待需要传入锁?你的条件变量不满足,不代表其他线程不满足,你需要归还锁给其他线程竞争使用! #### 3.2.2 同步的封装 **Cond.hpp** 封装条件变量唯一需要注意的是要传入一个锁,因此我们需要有一个锁的变量! ```cpp #pragma once #include namespace CondModule { class Cond { public: Cond(const Cond&) = delete; const Cond &operator=(const Cond&) = delete; Cond() { pthread_cond_init(&_cond, nullptr); } void Wait(pthread_mutex_t* mutex) { int n = pthread_cond_wait(&_cond, mutex); (void)n; } void Signal() { // 唤醒在条件变量下等待的一个线程 int n = pthread_cond_signal(&_cond); (void)n; } void Broadcast() { // 唤醒所有在条件变量下等待的线程 int n = pthread_cond_broadcast(&_cond); (void)n; } ~Cond() { pthread_cond_destroy(&_cond); } private: pthread_cond_t _cond; }; } ``` #### 3.2.3 为什么 pthread_cond_wait 需要互斥量? > * 条件等待是线程间同步的一种手段,如果只有一个线程,条件不满足,一直等下去都不会满足,所以必须要有一个线程通过某些操作,改变共享变量,使原先不满足的条件变得满足,并且友好的通知等待在条件变量上的线程 > > * 条件不会无缘无故的突然变得满足了,必然会牵扯到共享数据的变化。所以一定要用互斥锁来保护。没有互斥锁就无法安全的获取和修改共享数据 ![](https://i-blog.csdnimg.cn/direct/60f167f117ad41bdb5069256ef557e83.png) 核心原因:**原子性** `pthread_cond_wait` 需要互斥量的核心目的是为了保证 **"检查条件"** 和 **"进入等待"** 这两个操作的原子性。若没有互斥量,可能因线程竞争导致 **信号丢失** 或 **条件误判**,进而引发死锁或逻辑错误。 *** ** * ** *** 错误设计示例的问题: ```cpp pthread_mutex_lock(&mutex); while (condition_is_false) { pthread_mutex_unlock(&mutex); pthread_cond_wait(&cond); // 错误点! pthread_mutex_lock(&mutex); } pthread_mutex_unlock(&mutex); ``` 问题在于:**解锁(** `unlock`**)和等待(** `pthread_cond_wait`**)是分离的非** **原子操作**。 在解锁后、进入等待前,可能发生以下事件: 1. 其他线程获取到互斥量,修改了条件变量并发送信号(`pthread_cond_signal`)。 2. 原线程尚未进入等待状态,导致信号被**完全错过**。 3. 原线程最终在 `pthread_cond_wait` 处永久阻塞(因为条件已满足但信号已丢失)。 *** ** * ** *** 正确设计:`pthread_cond_wait` 的机制: ```cpp pthread_mutex_lock(&mutex); while (condition_is_false) { pthread_cond_wait(&cond, &mutex); // 原子操作! } // 条件满足时执行操作 pthread_mutex_unlock(&mutex); ``` * 给条件发送信号代码 ```cpp pthread_mutex_lock(&mutex); // 设置条件为真 pthread_cond_signal(cond); pthread_mutex_unlock(&mutex); ``` 1. **原子性释放锁并进入等待** : 调用 `pthread_cond_wait` 时,函数会**自动释放** **互斥** **量** ,并让线程阻塞在条件变量 `cond` 上。**解锁和等待是** **原子操作**,中间不会有其他线程插入修改条件变量。 2. **被唤醒后重新获取锁** : 当其他线程调用 `pthread_cond_signal` 或 `pthread_cond_broadcast` 时,原线程被唤醒,**自动重新获取** **互斥** **量**,继续执行后续代码。 *** ** * ** *** 为什么必须用互斥量? 1. **保护共享数据** : 条件的判断(如 `condition_is_false`)通常依赖共享数据(例如全局变量)。必须通过互斥量保证线程安全访问这些数据。 2. **避免信号丢失** : 只有通过 `pthread_cond_wait` 的原子操作,才能确保在释放锁和进入等待之间不会有其他线程修改条件并发送信号。 3. **防止虚假唤醒** : 即使没有信号,某些系统实现可能导致线程被唤醒(称为"虚假唤醒")。通`while(condition_is_false)` 循环`while`检查条件,可以避免逻辑错误。 ## 4. 生产者/消费者模型 ![](https://i-blog.csdnimg.cn/direct/b16458e79a7f46faae6372ce765af667.png) **三种要素:** 1. 生产者p 2. 消费者c 3. 一个交易场所 **"321"原则:** 1. 三种关系: 1. 生产者之间:互斥关系(竞争) 2. 消费者之间:互斥关系(竞争) 3. 生产者和消费者之间:互斥关系,同步关系 2. 两种角色 1. 生产者和消费者 3. 一个交易场所 1. 以特定结构构成的一块"内存"空间! **生产者消费者模型的好处:** 1. 生产过程和消费过程解耦 --\> 生产归工厂管,消费归消费者管,不同批次的线程执行不同的任务。 2. 支持忙闲不均。--\> 生产者可以生产很快/慢,消费者也可以消费很快/满。 3. 提供效率。 --\> 主要体现在工厂生产到超市存储,消费者消费之前生成的产品,同时工厂还可以继续生产!生产者和消费者并行! ### 4.1 基于BlockingQueue的生产者消费者模型 ![](https://i-blog.csdnimg.cn/direct/088c84f0592b4150976202db7f926235.png) > 在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出。 ### 4.3 封装实现生产者/消费者模型 ```cpp #pragma once #include "mutex.hpp" #include "cond.hpp" #include /* 基于 cond.hpp mutex.hpp */ #define defaultcap 5 namespace BlockQueueModule { template class BlockQueue { private: BlockQueue(const BlockQueue&) = delete; const BlockQueue &operator=(const BlockQueue&) = delete; bool IsFull() { return _q.size() >= _cap; } bool IsEmpty() { return _q.empty(); } public: BlockQueue() : _cap(defaultcap), _p_size(0), _c_size(0) { } void Enqueue(const T& data) { /* 加锁 */ _lock.Lock(); while(IsFull()) /* 如果队列为满,生产者需要等待 用while不用if是为了防止伪唤醒 */ { _p_size++; std::cout << "生产者,进入休眠了: _p_size: " << _p_size << std::endl; _p_cond.Wait(_lock.GetMutex()); _p_size--; } /* 阻塞队列不满,可以入数据 */ _q.push(data); if(_c_size > 0) { /* 如果存在消费者等待唤醒执行任务 */ std::cout << "唤醒消费者..." << std::endl; _c_cond.Signal(); } /* 解锁 */ _lock.Unlock(); } void Pop(T* out) { /* 加锁 */ _lock.Lock(); while(IsEmpty()) /* 如果队列为空,消费者需要等待,用while不用if是为了防止伪唤醒 */ { _c_size++; std::cout << "消费者,进入休眠了: _c_size: " << _c_size << std::endl; _c_cond.Wait(_lock.GetMutex()); _c_size--; } *out = _q.front(); _q.pop(); if(_p_size > 0) { /* 如果存在生产者等待唤醒执行任务 */ std::cout << "唤醒生产者者..." << std::endl; _p_cond.Signal(); } /* 解锁 */ _lock.Unlock(); } ~BlockQueue() { } private: MutexModule::Mutex _lock; /* 锁 */ /* 两个条件变量 */ CondModule::Cond _p_cond; /* 生产者变量 */ CondModule::Cond _c_cond; /* 消费者者变量 */ /* 阻塞队列 */ std::queue _q; int _cap; /* 阻塞队列最大容量 */ /* 记录变量 */ int _p_size; /* 记录生产者等待数量的变量 */ int _c_size; /* 记录消费者等待数量的变量 */ }; } ``` **测试:** ```cpp #include #include #include #include #include #include "blockqueue.hpp" using namespace BlockQueueModule; #if 0 // V3 typedef std::function fun_t; template class Task { public: Task(std::vector &task, BlockQueue *bq) : _task(task), _bq(bq), _index(0) { } int GetIndex() { ++_index; _index %= _task.size(); return _index; } ~Task() {} std::vector _task; BlockQueue *_bq; int _index; }; void* Produce(void *args) { Task* tk = static_cast *>(args); /* 生产任务 */ while(true) { sleep(1); // 模拟生产耗时 tk->_bq->Enqueue(tk->_task[tk->GetIndex()]); std::cout << "生产一个任务... : " << std::endl; } } void* Consumer(void *args) { Task* tk = static_cast *>(args); while(true) { sleep(1); /* 消费任务 */ fun_t action; tk->_bq->Pop(&action); std::cout << "消费一个任务... : " << std::endl; action(); } } int main() { fun_t fun1 = []() { std::cout << "下载任务..." << std::endl; }; fun_t fun2 = []() { std::cout << "日志查看..." << std::endl; }; fun_t fun3 = []() { std::cout << "上传任务..." << std::endl; }; std::vector task = {fun1, fun2, fun3}; /* 申请一个阻塞队列 */ BlockQueue *bq = new BlockQueue(); Task* tk = new Task(task, bq); std::vector p(3); std::vector c(5); /* create */ for(int i = 0; i < p.size(); ++i) { pthread_create(&p[i], nullptr, Produce, (void*)tk); } for(int j = 0; j < c.size(); ++j) { pthread_create(&c[j], nullptr, Consumer, (void*)tk); } /* join */ for(int i = 0; i < p.size(); ++i) { pthread_join(p[i], nullptr); } for(int j = 0; j < c.size(); ++j) { pthread_join(c[j], nullptr); } return 0; } #endif #if 0 // v2 int cnt = 1; void* Produce(void *args) { BlockQueue *bq = static_cast *>(args); /* 生产任务 */ while(true) { // sleep(1); // 模拟生产耗时 bq->Enqueue(cnt++); std::cout << "生产一个任务... : " << cnt << std::endl; } } void* Consumer(void *args) { BlockQueue *bq = static_cast *>(args); while(true) { sleep(1); /* 消费任务 */ int data; bq->Pop(&data); std::cout << "消费一个任务... : " << data << std::endl; } } int main() { /* 申请一个阻塞队列 */ BlockQueue *bq = new BlockQueue(); /* 单单 */ // pthread_t p[1], c[1]; // pthread_create(&p[0], nullptr, Produce, (void*)bq); // pthread_create(&c[0], nullptr, Consumer, (void*)bq); // pthread_join(p[0], nullptr); // pthread_join(c[0], nullptr); /* 多多 */ std::vector p(5); std::vector c(3); for(int i = 0; i < p.size(); ++i) { pthread_create(&p[i], nullptr, Produce, (void*)bq); } for(int j = 0; j < c.size(); ++j) { pthread_create(&c[j], nullptr, Consumer, (void*)bq); } for(int i = 0; i < p.size(); ++i) { pthread_join(p[i], nullptr); } for(int j = 0; j < c.size(); ++j) { pthread_join(c[j], nullptr); } delete bq; return 0; } #endif #if 0 // V1 bool Ready = false; pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 共享互斥锁 pthread_cond_t cond_producer = PTHREAD_COND_INITIALIZER; // 生产者条件变量 pthread_cond_t cond_consumer = PTHREAD_COND_INITIALIZER; // 消费者条件变量 void* Produce(void* args) { std::string name = static_cast(args); while (true) { pthread_mutex_lock(&mutex); // 等待直到产品被消费(Ready == false) while (Ready == true) { pthread_cond_wait(&cond_producer, &mutex); } // 生产 std::cout << name << ",开始生产..." << std::endl; Ready = true; // 通知消费者 pthread_cond_signal(&cond_consumer); pthread_mutex_unlock(&mutex); sleep(1); // 模拟生产耗时 } return nullptr; } void* Consumer(void* args) { std::string name = static_cast(args); while (true) { pthread_mutex_lock(&mutex); // 等待直到产品就绪(Ready == true) while (Ready == false) { pthread_cond_wait(&cond_consumer, &mutex); } // 消费 std::cout << name << ",开始消费..." << std::endl; Ready = false; // 通知生产者 pthread_cond_signal(&cond_producer); pthread_mutex_unlock(&mutex); } return nullptr; } int main() { // 单生产单消费 pthread_t p; /* 生产者 */ pthread_t c; /* 消费者 */ pthread_create(&p, nullptr, Produce, (void*)"thread-p"); pthread_create(&c, nullptr, Consumer, (void*)"thread-c"); pthread_join(p, nullptr); pthread_join(c, nullptr); return 0; } #endif ```

相关推荐
xq5148631 小时前
Linux系统下安装mongodb
linux·mongodb
柒七爱吃麻辣烫1 小时前
在Linux中安装JDK并且搭建Java环境
java·linux·开发语言
fallzzzzz1 小时前
C++ stl中的list的相关函数用法
c++·list
孤寂大仙v2 小时前
【Linux笔记】——进程信号的产生
linux·服务器·笔记
深海蜗牛2 小时前
Jenkins linux安装
linux·jenkins
Moshow郑锴2 小时前
Spring Boot 3 + Undertow 服务器优化配置
服务器·spring boot·后端
悦悦子a啊2 小时前
PTA:jmu-ds-最短路径
c++·算法·图论
愚戏师2 小时前
Linux复习笔记(三) 网络服务配置(web)
linux·运维·笔记
JANYI20183 小时前
嵌入式MCU和Linux开发哪个好?
linux·单片机·嵌入式硬件
小王努力学编程3 小时前
高并发内存池(三):TLS无锁访问以及Central Cache结构设计
jvm·数据结构·c++·学习