目录
[1. 线程互斥](#1. 线程互斥)
[1.1 进程线程间的互斥相关背景概念](#1.1 进程线程间的互斥相关背景概念)
[1.2 互斥量mutex (新的数据类型)](#1.2 互斥量mutex (新的数据类型))
[1.2.1 互斥量的接口](#1.2.1 互斥量的接口)
[1.2.1.1 初始化互斥量](#1.2.1.1 初始化互斥量)
[1.2.1.2 销毁互斥量:](#1.2.1.2 销毁互斥量:)
[1.2.1.3 互斥量的加锁和解锁](#1.2.1.3 互斥量的加锁和解锁)
[1.3 互斥锁的实现原理](#1.3 互斥锁的实现原理)
[2.1 条件变量的概念和接口](#2.1 条件变量的概念和接口)
[2.2 同步概念与竞态条件](#2.2 同步概念与竞态条件)
[2.3 初步理解条件变量](#2.3 初步理解条件变量)
[2.4 demo代码,验证条件变量的功能](#2.4 demo代码,验证条件变量的功能)
[2.5 生产者消费者模型(重要)](#2.5 生产者消费者模型(重要))
[2.5.1 是什么??](#2.5.1 是什么??)
[2.5.2 为什么??](#2.5.2 为什么??)
[2.5.3 怎么办??](#2.5.3 怎么办??)
[2.6 基于BlockingQueue(阻塞队列)的生产者消费者模型 (条件变量 + 互斥锁 = 设计一个cp(生产者消费者)问题 -- 解释之前没有说清楚的问题)](#2.6 基于BlockingQueue(阻塞队列)的生产者消费者模型 (条件变量 + 互斥锁 = 设计一个cp(生产者消费者)问题 -- 解释之前没有说清楚的问题))
[2.7 生产者和消费者的周边问题](#2.7 生产者和消费者的周边问题)
[2.7.1 多生产者多消费者的代码:](#2.7.1 多生产者多消费者的代码:)
[2.7.2 高效体现在哪里?生产和消费的过程都是串行的呀?](#2.7.2 高效体现在哪里?生产和消费的过程都是串行的呀?)
[2.7.3 生产和消费,就是传递数据吗?](#2.7.3 生产和消费,就是传递数据吗?)
1. 线程互斥
1.1 进程线程间的互斥相关背景概念
- 共享资源:多执行流可以同时访问的资源称为共享资源
- 临界资源:多线程执行流被保护的共享的资源就叫做临界资源
- 临界区:每个线程内部,访问临界资源的代码,就叫做临界区
多线程/多进程往显示器上打印的时候,就会发生错乱的情况,发生错乱的情况:当多执行流访问同一个显示器文件的时候,显示器本质也是文件,向显示器打印本质就是向显示器文件写入,一旦多执行流访问同一个显示器,显示器本身就是共享资源,多执行流执行自己的打印临界区代码时,访问了同一个显示器,同一个显示器没有被保护起来,所以打印的消息就发生了错乱。显示器叫做临界资源,将这种错乱称为:数据发生不一致问题!!
非临界区的代码,执行的时候不会影响其他线程,不会影响执行结果;还有一部分的代码是临界区代码,是专门用来访问共享资源的。
访问临界区 or 临界资源的时候,比如在多线程那里 new/malloc 了一个全局对象,此时,这个全局对象也是可以被其他线程看见的呀,能看到不代表能访问,临界区访问临界资源,造成异常,并不是你们共享了资源,而是共享了资源的同时,多线程还进行了并发访问导致的。所以,共享资源不一定能导致数据不一致的问题,访问共享资源才会导致数据不一致的问题!!!这种问题如何解决???---- 同步 和 互斥!!!
- 互斥:任何时刻,互斥保证有且只有⼀个执行流进⼊临界区,访问临界资源,通常对临界资源起保护作⽤(例子:ATM独立房间)
互斥访问的时候没有其它线程来打扰,本质是将多线程访问资源由并发执行转换成串行执行。互斥带来的正面意义:可以保护共享资源的安全。但是也可能带来负面影响:降低程序的运行效率。
- 原⼦性(后⾯讨论如何实现):不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成
站在ATM机外面的人知道:自己敲门是没有意义的,必须在门口等你将钱取完,他才能访问ATM机才有意义。只有两种状态才对外面的人有意义:1. ATM机没有人访问 2. 上一个人操作完了,走了。 所以站在外面的人,里面的人的动作就是原子的,要么ATM机没有人访问,要么就是里面的人操作完,出来了。对外面的人来讲是有意义的,里面的人在访问期间,外面的人是产生不了影响的。取钱的动作对于外面的人来说就是原子性的。原子性不是一种特性,而是一种结果。因为有互斥的存在,而铸造了访问资源的原子性。
cpp
#include <cstdio>
#include <pthread.h>
#include <string>
#include <unistd.h>
int tickets = 1000; //全局资源
void* routine(void *args)
{
std::string name = static_cast<const char*>(args);
while(true)
{
//???
if(tickets > 0)
{
usleep(100); //线程更高频次的被挂起,意味着被高频次的被唤醒,意味着不断的被切换
printf("%s 抢占票号:%d\n",name.c_str(),tickets--);
}
else
{
break;
}
}
return (void*)0;
}
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);
}



所以,上面出现负数的主要原因是1这个问题。2这个问题出现并发问题一般是会将tickets这个值变大!!!越早保存,保存的数据的值就越大!!2这个问题是可能会让不同的线程买到同一张票!!!
所以:在以上的代码中
- 共享资源:tickets全局变量
- 临界资源:还没有被保护起来
- 临界区:
出现问题是因为:多线程并发的访问临界区代码,导致数据不一致的问题,所以要解决这个问题,本质就是要对:对代码进行保护,形成临界区!!!如何保护??---- pthread库,提出来了一个互斥锁的概念!!要求互斥锁是可编程的!!!
1.2 互斥量mutex (新的数据类型)
1.2.1 互斥量的接口
pthread库提供的数据类型:pthread_mutex_t ,全局变量 mutex,mutex 就叫做互斥锁。
1.2.1.1 初始化互斥量
初始化互斥量有两种方法:
- 方法1:静态分配
mutex 如果是静态的或者是全局的,直接用PTHREAD_MUTEX_INITIALIZER 来进行初始化:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
- 方法2:动态分配
如果是在栈上动态开辟的互斥锁,使用 pthread_mutex_init 来做初始化。
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const
pthread_mutexattr_t *restrict attr);
参数:
mutex:要初始化的互斥量
attr:NULL

1.2.1.2 销毁互斥量:
注意:
定义的互斥锁如果是全局的,不需要调用destroy销毁;如果是局部的需要调用destroy来销毁。
int pthread_mutex_destroy(pthread_mutex_t *mutex);
1.2.1.3 互斥量的加锁和解锁
int pthread_mutex_lock(pthread_mutex_t *mutex); //加锁
int pthread_mutex_unlock(pthread_mutex_t *mutex); //解锁
返回值:成功返回0,失败返回错误号
cpp
#include <cstdio>
#include <pthread.h>
#include <string>
#include <unistd.h>
int tickets = 1000; //全局资源
pthread_mutex_t gmutex = PTHREAD_MUTEX_INITIALIZER; //锁也是全局的!
void* routine(void *args)
{
std::string name = static_cast<const char*>(args);
while(true)
{
pthread_mutex_lock(&gmutex);
if(tickets > 0)
{
usleep(100); //线程更高频次的被挂起,意味着被高频次的被唤醒,意味着不断的被切换
printf("%s 抢占票号:%d\n",name.c_str(),tickets--);
pthread_mutex_unlock(&gmutex);
}
else
{
pthread_mutex_unlock(&gmutex);
break;
}
}
return (void*)0;
}
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);
}
运行结果:

此时票号就不会出现负数了。
加锁之后的效率明显是变慢了的。
问题1:
1. gmutex自己不就是全局变量吗???你要保护别人,你怎么保证你自己是安全的???
gmutex是原子的! pthread_mutex_lock(&gmutex); 和 pthread_mutex_unlock(&gmutex); 这样的操作是会被做成原子的!!!不会在申请锁的时候出现并发问题。但是为什么是原子的呢?--- 互斥锁的底层实现!!
问题2:
2. 所有的线程都要先申请锁吗??
是的,所有线程都必须遵守该规则!!!如果故意有3个线程都申请锁了,有一个线程没有申请锁,这是在写bug!被漏掉的线程和正常访问的线程之间依旧存在并发问题!!所以加锁的问题必须大家都得申请。申请成功,线程会继续向后运行;申请失败,线程会被阻塞。直到有人释放才会向后运行
问题3:
3. 加锁和解锁之间,如果线程我进入了临界区,线程会被切换吗??--- 会被切换 。 会因为切换,导致并发问题吗?
例子引入:解释会因为切换,导致并发问题吗?

场景1:一个学生A早早的就来自习了,拿着钥匙进入超级自习室中学习,其它后续来的学生就只能在外面等着。学生A中午去吃饭,将钥匙挂回原处,学生们看谁跑得快,谁就能够先拿到钥匙去自习。
场景2:你定了外卖,你要去拿,但是现在你不想其它的学生进入自习室,此时你将门关上,钥匙不给任何人,就去那外卖,即使你人现在不在自习室中,因为你把门锁着,钥匙也拿走了,即使外面的学生想进入自习室中也是进入不了的,因为他们没有钥匙。
超级自习室就是CPU,对应的钥匙就是锁资源,每一个学生就是线程,你去拿外卖,你人即使不在自习室中,其他人也是进入不了自习室的,就相当于你被切换了,但是你没有释放对应的锁,其他人照样进不去。
所以:会因为切换,导致并发问题吗? -- 不会!!你是持有锁被切换的!!其它线程依旧不会被唤醒,不能被运行,不能进入临界区。所有线程只能等你回来,继续运行,直到你把锁释放,所有的线程才能竞争锁,进入临界区。所以这就是为什么加锁之后,抢票的效率比较慢的原因之一。这也是所有线程为什么串行执行的根本原因!
原子性在两个场景下,是在加锁之后诞生的结果
定义的锁是在栈上开辟的:
cpp
#include <cstdio>
#include <pthread.h>
#include <string>
#include <unistd.h>
struct data
{
std::string name;
pthread_mutex_t *lockp;
};
int tickets = 1000; //全局资源
// 1. gmutex自己不就是全局变量吗???你要保护别人,你怎么保证你自己是安全的???原子的!!但是为什么是原子的呢?
// pthread_mutex_t gmutex = PTHREAD_MUTEX_INITIALIZER; //锁也是全局的!
void* routine(void *args)
{
data *d= static_cast<data*>(args);
while(true)
{
// 4.
// 2. 所有的线程都要先申请锁吗??是的,所有线程都必须遵守该规则!!!
pthread_mutex_lock(d->lockp); // 2.1 大家都要申请,申请成功,线程会继续向后运行;申请失败,线程会被阻塞。会卡在这里,直到有人释放才会向后运行
if(tickets > 0)
{
// 3.加锁和解锁之间,如果线程我进入了临界区,线程会被切换吗??--- 会被切换。会因为切换,导致并发问题吗? -- 不会!!你是持有锁被切换的!!
usleep(100); //线程更高频次的被挂起,意味着被高频次的被唤醒,意味着不断的被切换
printf("%s 抢占票号:%d\n",d->name.c_str(),tickets--);
pthread_mutex_unlock(d->lockp);
}
else
{
pthread_mutex_unlock(d->lockp);
break;
}
}
return (void*)0;
}
int main()
{
pthread_mutex_t lock;
pthread_mutex_init(&lock,nullptr /*属性*/);
data d1 = {"thread-1",&lock};
pthread_t t1,t2,t3,t4;
pthread_create(&t1,nullptr,routine,(void*)&d1);
data d2 = {"thread-2",&lock};
pthread_create(&t2,nullptr,routine,(void*)&d2);
data d3 = {"thread-3",&lock};
pthread_create(&t3,nullptr,routine,(void*)&d3);
data d4 = {"thread-4",&lock};
pthread_create(&t4,nullptr,routine,(void*)&d4);
pthread_join(t1,nullptr);
pthread_join(t2,nullptr);
pthread_join(t3,nullptr);
pthread_join(t4,nullptr);
pthread_mutex_destroy(&lock);
return 0;
}
运行结果:


保护临界资源本质是保护临界区,保护临界区本质是对临界区做加锁。

所有的线程访问临界区的时候,都得申请锁的前提是都得先看到锁,所以锁本身就是临界资源!!!你怎么保证自身安全???怎么保证如果线程切换的话,加锁和解锁是安全的???所以加锁和解锁的操作本身被设计成原子的!!

原子是如何保证的??
互斥锁又是如何设计的??
1.3 互斥锁的实现原理
锁的实现:
1. 硬件实现方案:关闭中断!
2. 软件实现方案:一条汇编语句
概念:x86计算机往往会给我们提供两条汇编指令:swap 或 exchange指令,该指令的作用是把寄存器和内存单元的数据相交换
加锁:



以上便是互斥锁实现的软件实现方案,实际上我们现在用到的锁都是软件的方案。OS更倾向于使用硬件方案,做的工作往往比较重要!!软件的实现方案会慢一点点,会涉及到访存操作。
解锁:
将 1 写入 mutex 中就可以了,相当于就把这个 1 归还给 mutex 了。
加锁和解锁的实现往往不能太复杂!!!
回到超级自习室的例子中:

cpp
void* routine(void *args)
{
data *d= static_cast<data*>(args);
while(true)
{
// 4.
// 2. 所有的线程都要先申请锁吗??是的,所有线程都必须遵守该规则!!!
pthread_mutex_lock(d->lockp); // 2.1 大家都要申请,申请成功,线程会继续向后运行;申请失败,线程会被阻塞。会卡在这里,直到有人释放才会向后运行
if(tickets > 0)
{
// 3.加锁和解锁之间,如果线程我进入了临界区,线程会被切换吗??--- 会被切换。会因为切换,导致并发问题吗? -- 不会!!你是持有锁被切换的!!
usleep(100); //线程更高频次的被挂起,意味着被高频次的被唤醒,意味着不断的被切换
printf("%s 抢占票号:%d\n",d->name.c_str(),tickets--);
pthread_mutex_unlock(d->lockp);
}
// else
// {
// pthread_mutex_unlock(d->lockp);
// break;
// }
}
return (void*)0;
}
四个线程优先级竞争锁的能力等价,票已经被抢完了,在之后规定的时间段才会放票,但是在规定时间前却一直做着申请锁,判断条件不成立,释放锁,申请锁,判断,释放锁的工作,就是在白白地浪费CPU资源。只有互斥功能在很多情况下是可以用的,比如:短期之内做一个互斥功能,一做就完了,就没问题;需要重复的做,需要依赖于特定条件的,去完成访问控制的话,光有互斥锁是没错,但是不合理,因为可能导致效率低下的问题。

互斥是赤裸裸的保证共享资源临界资源的安全性,更侧重安全性。同步也是可以保证安全的,也可以保证线程访问资源有一定的顺序性!!这里的顺序性就可以有效的规避一个线程因为条件不满足而频繁的申请锁的情况。根本原因是为了保证安全的情况下,让我们的线程工作起来,协同起来,更高效,因为没有饥饿问题了!!
大语言形成的简单的抢票demo:
cpp
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>
std::mutex mtx;
int tickets = 100;
void sellTickets(const std::string& name) {
while (true) {
mtx.lock(); // 手动加锁
if (tickets > 0)
{
std::cout << name << " 抢到票号:" << tickets << std::endl;
tickets--;
mtx.unlock(); // 手动解锁
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
else {
mtx.unlock(); // 记得解锁
std::cout << name << " 票已售完" << std::endl;
break;
}
}
}
int main() {
std::thread t1(sellTickets, "窗口-1");
std::thread t2(sellTickets, "窗口-2");
std::thread t3(sellTickets, "窗口-3");
std::thread t4(sellTickets, "窗口-4");
t1.join();
t2.join();
t3.join();
t4.join();
std::cout << "\n剩余票数:" << tickets << std::endl;
return 0;
}
锁的封装:
cpp
//testMutex.cc
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sched.h>
#include "../Mutex/Mutex.hpp"
int ticket = 1000;
Mutex lock;
void *route(void *args)
{
char *id = (char*)args;
while(1)
{
lock.Lock();
if(ticket>0)
{
usleep(1000);
printf("%s 抢占票号:%d\n",id,ticket);
ticket--;
lock.Unlock();
}
else
{
lock.Unlock();
break;
}
}
return nullptr;
}
int main()
{
pthread_t t1,t2,t3,t4;
pthread_create(&t1,NULL,route,(void*)"thraed-1");
pthread_create(&t2,NULL,route,(void*)"thraed-2");
pthread_create(&t3,NULL,route,(void*)"thraed-3");
pthread_create(&t4,NULL,route,(void*)"thraed-4");
pthread_join(t1,nullptr);
pthread_join(t2,nullptr);
pthread_join(t3,nullptr);
pthread_join(t4,nullptr);
return 0;
}
cpp
//Mutex.hpp
#pragma once
#include <iostream>
#include <mutex>
#include <pthread.h>
class Mutex
{
public:
Mutex()
{
pthread_mutex_init(&_lock, nullptr);
}
void Lock()
{
pthread_mutex_lock(&_lock);
}
void Unlock()
{
pthread_mutex_unlock(&_lock);
}
~Mutex()
{
pthread_mutex_destroy(&_lock);
}
private:
pthread_mutex_t _lock;
};
进一步加深:
cpp
//Mutex.hpp
#pragma once
#include <iostream>
#include <mutex>
#include <pthread.h>
class Mutex
{
public:
Mutex()
{
pthread_mutex_init(&_lock, nullptr);
}
void Lock()
{
pthread_mutex_lock(&_lock);
}
void Unlock()
{
pthread_mutex_unlock(&_lock);
}
~Mutex()
{
pthread_mutex_destroy(&_lock);
}
private:
pthread_mutex_t _lock;
};
//LockGuard 是一个类 需要将锁传进来,对锁进行了包装
class LockGuard
{
public:
// 传进来一个外部锁的地址
LockGuard(Mutex *_mutex):_mutexp(_mutex)
{
_mutex->Lock();
}
~LockGuard()
{
_mutexp->Unlock();
}
private:
Mutex *_mutexp;
};
cpp
//testMutex.cc
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sched.h>
#include "../Mutex/Mutex.hpp"
int ticket = 1000;
Mutex lock; //锁对象是自己定义好的
void *route(void *args)
{
char *id = (char*)args;
while(1)
{
LockGuard lockguard(&lock); //RAII风格的加锁! -- lockguard 类变量是有局部性的,就完成对以下代码的加锁和解锁!!!
if(ticket>0)
{
usleep(1000);
printf("%s 抢占票号:%d\n",id,ticket);
ticket--;
}
else
{
break;
}
} //以{}为单位,构建了一个临界区
return nullptr;
}
int main()
{
pthread_t t1,t2,t3,t4;
pthread_create(&t1,NULL,route,(void*)"thraed-1");
pthread_create(&t2,NULL,route,(void*)"thraed-2");
pthread_create(&t3,NULL,route,(void*)"thraed-3");
pthread_create(&t4,NULL,route,(void*)"thraed-4");
pthread_join(t1,nullptr);
pthread_join(t2,nullptr);
pthread_join(t3,nullptr);
pthread_join(t4,nullptr);
return 0;
}
所以,大语言模型给我们生成的时候用的就是lock_guard 这种风格,在循环中是一个临时的类变量,具有局部性,创建的时候就加锁,循环结束就会被自动释放 -- 解锁!!

但是,如果未来代码中还有其他的代码呢? --- 再套一个花括号,将lockguard 囊括进去


2.线程同步
要想将线程同步起来,就得引入线程库提供的新特性,这个新特性就是条件变量。
2.1 条件变量的概念和接口
概念:
- 当⼀个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。(比如,我们抢票,票数已经为0了,其他线程不改票数的情况下,这个线程只能是申请锁,释放锁,什么都做不了,导致所浪费,导致其他线程竞争不到锁资源的情况)
- 例如⼀个线程访问队列时,发现队列为空,它只能等待,只到其它线程将⼀个节点添加到队列中。这种情况就需要⽤到条件变量。
如果想使用同步,在 pthread库 中提供的是条件变量,条件变量的数据类型是 pthread_cond_t
接口:和互斥锁的接口相似

条件变量有三个特殊技接口:
pthread_cond_wait():

有一个特点:线程在进行访问时,如果判断某些条件不满足,例如抢票,票为0了,不想让它break,想让线程去等待。让我们的指定线程在指定的条件变量下去等待,但是去等的时候,第二个参数那里带了一个互斥锁。这里为什么要有锁,不是很清楚,之后在代码的书写的时候会解释。所以,可以让一个线程在指定的条件变量下去等待。只要等了,就可以让别人访问完,别人再将它唤醒,它再去拿数据 or 取数据 or 访问数据,这样,线程就具有一定的顺序了。
pthread_cond_signal():唤醒在该环境变量下的等待的线程。
等的线程可能不止一个,可能会有多个等待的线程,pthread_cond_broadcast():唤醒所有在该条件变量下等待的线程

2.2 同步概念与竞态条件
- 同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步
- 竞态条件:因为时序问题,⽽导致程序异常,我们称之为竞态条件。在线程场景下,这种问题也不难理解
2.3 初步理解条件变量
例子:有两个学生,都蒙着眼睛,学生A往盘子中放苹果,学生B在盘子中拿苹果。
A正准备往盘子中放苹果,B会拿到苹果吗?双方并不知道彼此的存在,A放苹果不知道是否成功,导致B拿到苹果也是不确定的,二义性,有可能拿到,有可能没拿到,可能会导致数据不一致的问题,所以至少保证AB拿放苹果这件事能够正常的进行,所以引入一把锁:

AB不管是拿还是放都需要先竞争锁,竞争锁成功的人才能做对应的动作。A竞争到锁 -> 放苹果 -> 释放锁 -> 最后再返回;B竞争到锁 -> 有,拿苹果 / 没有 -> 释放锁 -> 最后再返回。这样放和拿的状态都是确定的。
AB都是不知道盘子中是否有苹果,所以就得高频率的去申请锁,站在A的视角,A这个人拿到锁了,在放苹果之前检测盘子中是否有苹果,如果有苹果,他就不放了,释放锁,如果没有就放苹果。将苹果放入盘子中,但是知不知道下一次多久放呢?站在放的角度是不知道的。放完苹果后,不知道苹果有没有被对方拿走,所以,A只有放完苹果之后立即再去申请锁,如果没有苹果,再去放苹果就是了,如果检测到有苹果,就释放锁,释放锁之后,又不确定对方拿走没有,A就得再去申请锁,检测,释放。假设B拿苹果特别慢,A隔2秒放一个,B隔1分钟拿一次,会导致线程A在高频次的做无用工作,如果A的竞争能力特别强,就会导致A一直申请锁,释放锁,导致B长时间拿不了苹果,让A一直做无用功,所以这种纯互斥的做法没有错,但是不高效!!!对B也是同样如此,B线程把苹果拿走了,并不知道对方有没有再放,就会高频次的去申请,释放的工作,对方如果竞争锁的能力特别差,B就会频繁的申请锁、释放锁,每多申请一次,导致A放苹果的机会就会少一次,这种做法是没错,但是效率一定会比较低下。
**以学生A,放苹果的人为主导,解决高频次去申请锁的问题,引入一个铃铛。**A对B说,我放完苹果就会敲一下铃铛,你第一次拿到苹果之后,第二次没有拿到的话,就不要申请锁了,你去铃铛那里等,等我敲铃铛,我一敲,你又过去拿。
A放了苹果,B也拿到苹果了,B并不清楚A有没有再放,B又去申请锁,发现盘子中并没有苹果了,此时B就去铃铛那里等了,从此以后,这个锁就再也没有和A抢了,过了一会,A又申请锁,放苹果,释放锁,A就敲一下铃铛,B知道盘子中有苹果了,就又跑过去申请锁,把这个苹果拿到,拿到之后释放锁,不知道A又放没放,就会再次申请锁,盘子中没有苹果,B就会又去铃铛那里等

所以,双方在保证放拿苹果安全的情况下,还让双方放拿苹果具有了一定的顺序性,这个过程就是同步的过程。
在整个过程当中,锁是需要的,叫做互斥锁,铃铛相当于条件变量,学生相当于就是线程,线程B在条件变量下排队的过程,就相当于pthread_cond_wait,线程A将苹果放完,敲这个铃铛就相当于唤醒其中一个在条件变量下等的线程。
B一旦发现条件不满足,就会去条件变量下等待,A一旦发现盘子中有苹果了,允许拿了,A就会signal,唤醒其中一个线程。

放拿苹果的各有n个人也是不会出错的!!因为放和拿大家都要申请锁,工作流协作的时候,是不会出错的。只不过拿苹果的人老是被同一个人拿,会导致其他线程饥饿的问题,这种情况是存在的。
假设:放苹果1个人,拿苹果3、4个人都要拿呢?以放苹果的人为主,放拿苹果的过程就是典型的生产和消费的过程:

放苹果的人放进来了,一个人申请锁,拿苹果,释放锁,再次申请锁,发现没有苹果,到铃铛那里去等了,拿苹果的人竞争能力很强,后续的人再次申请锁,没有苹果,释放锁,全部都到铃铛队列中去等待,条件变量是允许同时存在多个线程等待的,后来,线程A放了一个苹果,敲一下,pthread_cond_signal 代表唤醒一个头部的线程,去拿苹果。A敲了两声,pthread_cond_broadcast 唤醒所有等待的线程,让他们也去竞争竞争,抢到的人会拿苹果,抢不到的人,等拿到锁时条件已假 -> 回条件变量队列。

2.4 demo代码,验证条件变量的功能
主线程控制新线程运行??

cpp
void *active(void* args)
{
std::string name = static_cast<const char*>(args);
while(true)
{
pthread_mutex_lock(&gmutex);
pthread_cond_wait(&gcond, &gmutex); //直接休眠,在临界区内部
std::cout << name << "active!" << std::endl; //对显示器进行保护
pthread_mutex_unlock(&gmutex);
}
return (void*)0;
}
上面的代码。不是说所有的线程疯狂的申请锁,疯狂的像显示器不停的打印,而是说我先让所有线程等待,我让你打印,你才打印。
也就是说,未来创建的线程1-4,每一个线程都会先申请锁,持有锁的人才允许你打印,申请锁之后不让你打印,而是直接让你在条件变量下去等,唤醒你时,才让你去打印。看到此现象,让主线程sleep(5); 5秒以后才会让你的线程进行所谓的打印,一旦启动起来程序在5秒以内是没有任何的输出。因为:主线程不做输出,新线程一旦申请到锁,立马去休眠了,不去唤醒它。所有的线程都去指定的条件变量下去等待了,主线程需要去不断地去唤醒指定的线程。每隔1秒唤醒一个进程。
cpp
#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>
pthread_cond_t gcond = PTHREAD_COND_INITIALIZER; //定义成全局 or 静态的,所有线程到要去条件变量下去等,前提是都要看到同一个条件变量
pthread_mutex_t gmutex = PTHREAD_MUTEX_INITIALIZER;
void *active(void* args)
{
std::string name = static_cast<const char*>(args);
while(true)
{
pthread_mutex_lock(&gmutex);
pthread_cond_wait(&gcond, &gmutex); //直接休眠,在临界区内部
std::cout << name << " active!" << std::endl; //对显示器进行保护
pthread_mutex_unlock(&gmutex);
}
return (void*)0;
}
int main()
{
pthread_t t1,t2,t3,t4;
pthread_create(&t1, nullptr, active, (void*)"thread-1");
pthread_create(&t2, nullptr, active, (void*)"thread-2");
pthread_create(&t3, nullptr, active, (void*)"thread-3");
pthread_create(&t4, nullptr, active, (void*)"thread-4");
sleep(5);
while(true)
{
pthread_cond_signal(&gcond);
sleep(1);
}
pthread_join(t1,nullptr);
pthread_join(t2,nullptr);
pthread_join(t3,nullptr);
pthread_join(t4,nullptr);
}
运行出来的结果不一定是1->2->3->4,而是有一定的规律:

具有一定明显的顺序,说明所有的线程本质上都是先去条件变量下去排队的,每一秒钟,唤醒一个线程,执行一次,打印一次,具有明显的顺序性。
一次唤醒所有的线程:
cpp
while(true)
{
// pthread_cond_signal(&gcond);
pthread_cond_broadcast(&gcond);
sleep(1);
}
运行结果:

顺序出现错乱,原因是唤醒之后还是要去竞争锁的,谁先抢到,也是会影响打印的顺序的!!唤醒一批线程,让一批线程帮我们完成任务。
2.5 生产者消费者模型(重要)
2.5.1 是什么??

一个线程想把自己的数据给另一个线程,直接交付的话,会存在比较强的耦合关系,所以中间加一个交易行所,一个线程把数据写入到中间的内存块里面,写好之后,另一个线程再从内存块中读走。这样做的好处是:
1. 维护松耦合关系
2. 支持生产和消费的忙闲不均
3. 减少生产和消费过程中产生的成本!
生产者有可能是1 or 多个线程,消费者也有可能是1 or 多个线程,所以有没有可能正在生产时,消费者就来消费呢?今天正在放一根火腿肠时,你就来消费,你是有可能会拿到,有可能不会拿到,不确定。生产者和消费者同时访问这个区,这个区就是典型的公共资源,生产者和消费者并发访问时就需要对这个公共资源进行保护,变为一个临界资源。数据放入内存块中,在放入之前就要保证放入数据的安全,读取数据也是一样的。生产者和消费者也有自身所对应的临界区代码。临界区代码就是负责生产和消费访问临界区资源的。

生产者和消费者之间互斥的关系:
将内存块当成整体,正在往内存块中写入数据的时候,消费者是不能来消费的,消费者正在消费,生产者是不能来生产的,生产者消费者之间首先要维护的关系是互斥。
生产者和消费者之间同步的关系:
超市没有货了,通知生产者来生产,生产完了通知消费者来消费,消费完了再通知生产者。具有一定的强烈的顺序性。

后面写代码才能回答上面的问题。
**生产消费者模型是:**通过维护提供中间的一个交易场所,再引入两种角色,一个生产者,一个消费者,通过维护生产和消费的3种关系,用锁技术,用同步技术,把多线程之间协同起来,工作起来,这个就是生产者消费者模型。
2.5.2 为什么??

为什么要有生产者消费者模型?
因为生产者和消费者之间是松耦合关系,因为是松耦合就可以支持生产者和消费者之间忙闲不均,同时代码上也好维护,进而达到一个减少生产和消费过程中产生的成本问题!!
生产者消费者模型最主要的变化是交易场所一直在变,多个线程一会用数组来通信,一会用链表通信,一会用队列通信,最常见的就是队列,交易场所的变化会引起编码细节上的变化,其它的大差不差。
2.5.3 怎么办??
会在 2.6 中展开来说。
2.6 基于BlockingQueue(阻塞队列)的生产者消费者模型 (条件变量 + 互斥锁 = 设计一个cp(生产者消费者)问题 -- 解释之前没有说清楚的问题)
在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现实现生产者和消费者模型的数据结构。其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的,线程在对是阻塞队列进程操作时会被阻塞)
之前的管道也是有这样的特性的,写满了再写入会阻塞,为空了再读也会阻塞。所以之前**管道的本质就是:进程间的生产者消费者模型!!!**所以在管道当中会自动维护互斥和同步的关系,尤其是同步的关系!!!管道本身:就是一个阻塞队列 -- 队列的元素是字节!!!
所以:基于一个BlockingQueue(阻塞队列)的生产者消费者模型,就相当于类似于管道的应用层代码,管道就是典型的单生产者单消费者模型。
设计一个阻塞队列,一个线程从阻塞队列放数据,另一个线程从阻塞队列当中拿数据,如果满了,让这个线程阻塞掉,如果拿数据为空了,也要让这个线程阻塞掉,不空也不满,让这两个线程正常工作。这个就叫做多线程之间基于阻塞队列完成生产者消费者模型。
单生产单消费基于阻塞队列通信的逻辑:
单生产单消费比较简单是因为不用维护生产者生产者之间的关系和的消费者和消费者之间的关系,只需要维护生产者和消费者之间的关系,但是生产者在生产时,消费者是有可能回来消费的,所以得加锁。
cpp
//BlockQueue.hpp
#pragma once
#include <iostream>
#include <string>
#include <queue>
#include <pthread.h>
const static uint32_t gcap = 5; // 默认的
template <typename T> // 引入模板,里面传的数据类型不限制
class BlockQueue
{
private:
bool IsFull()
{
return _bq.size() >= _cap;
}
bool IsEmpty()
{
return _bq.empty();
}
public:
BlockQueue(uint32_t cap = gcap) : _cap(cap)
{
pthread_mutex_init(&_lock, nullptr); // 锁的初始化
pthread_cond_init(&_c_cond, nullptr); // 消费者的条件变量初始化
pthread_cond_init(&_p_cond, nullptr); // 生产者的条件变量初始化
}
// 纯输入的话,const 引用
void Enqueue(const T &in)
{
pthread_mutex_lock(&_lock);
// 要进行生产,就一定能够进行生产吗? -- 不一定!!一定要满足生产条件!!
// 队列不为满的时候才能生产
if (IsFull()) // 满了,不能再生产了
{
// 问题1:我在进行等待的时候,我可是在临界区里等的啊!!我可是持有锁的!!申请者也不可能申请锁成功,完成消费
// 所以,需要自动让线程 释放锁!!!所以 pthread_cond_wait 第二个参数就是你要释放的锁!!!
// 问题2:我为什么要把自己弄得需要在临界区内部等??先判断队列是否为满(生产条件是否满足)
// 判断队列是否为满,本身就是访问临界资源!!
// 判断队列是否为满,必须在临界区内部判断
// 所以生产者必须先申请锁,在临界区内部判断
// 判断未满的结果,需要等待的结构,也一定在临界区内部!!
// 所以,当代的时候,在内部释放锁是必然的!
// 所以,锁被做到了 pthread_cond_wait的参数中!!!
pthread_cond_wait(&_p_cond, &_lock); // 特征1:自动释放锁!! //特征2:唤醒之后,自动重新竞争锁并持有锁!!
// 当我们被唤醒的时候,就一定又从这个位置唤醒了!!(锁已经被释放掉了)
// 是在临界区内被唤醒的!!!
// 唤醒之后,pthread_cond_wait自动重新竞争锁,有可能会竞争失败,竞争失败,pthread_cond_wait就在锁那里阻塞住,
// 一直不被唤醒,直到申请锁成功了,才能继续往后走,这样才能保证此线程在运行的时候,去条件变量下等的时候自动释放锁
// 当它被唤醒又会参与锁的竞争,有可能会失败,又去锁那里等了,等到锁了 pthread_cond_wait这个函数才会彻底返回,继续做生产
// pthread_cond_wait函数保证往后走的时候是持有锁的状态来生产的
}
// 认为队列没满 -- 可生产
_bq.push(in); // 完成生产
// 一定有一个数据
pthread_cond_signal(&_c_cond); // 唤醒消费者 可放在这里 最佳推荐 放在里面
pthread_mutex_unlock(&_lock);
// pthread_cond_signal(&_c_cond); // 唤醒消费者 也可放在这里
// pthread_cond_signal放在里面和外面是等价的原因:因为唤醒之后都要去竞争锁,
// 只要去竞争锁了,就只有一个人持有,代码不会乱!!!
// 唤醒放在里面,unlock一解锁, pthread_cond_wait(&_c_cond, &_lock);就申请锁了
// pthread_cond_signal(&_c_cond); 、
// pthread_mutex_unlock(&_lock);
// 唤醒放在外面,unlock释放锁,再唤醒另外的线程,释放锁和唤醒之间有可能被别的线程申请到锁了
// pthread_cond_wait(&_c_cond, &_lock);就会竞争失败,转而就去锁那里去等待了,其他线程
// 一释放,你就会申请成功
// pthread_mutex_unlock(&_lock);
// pthread_cond_signal(&_c_cond);
}
// 只有消费者最清楚,该你生产者生产了;只有生产者最清楚,该你消费者消费了
// 纯输出用指针
void Pop(T *out)
{
pthread_mutex_lock(&_lock);
// 想消费,不一定能消费,如果阻塞队列中没有数据了,就消费不了
if (IsEmpty())
{
// 消费者去等待,直到队列中有数据
pthread_cond_wait(&_c_cond, &_lock);
}
*out = _bq.front();
_bq.pop();
// 一定有一个空间
pthread_cond_signal(&_p_cond); // 唤醒生产者
pthread_mutex_unlock(&_lock);
}
// 既是输入参数又是输出参数:用引用
~BlockQueue()
{
pthread_mutex_destroy(&_lock);
pthread_cond_destroy(&_c_cond);
pthread_cond_destroy(&_p_cond);
}
private:
// 队列、容量,都是属于类类的一种全局数据 ------ 临界资源
std::queue<T> _bq; // blockqueue 要用到的阻塞队列
uint32_t _cap; // 容量 queue会自动扩容,所以要有一个上限的规定
pthread_mutex_t _lock;
// 2个条件变量:生产满了,让生产者去等;消费完了,让消费去等;同时还要能够唤醒彼此
pthread_cond_t _c_cond; // 消费者用的条件变量
pthread_cond_t _p_cond; // 生产者用的条件变量
// 其实,生产者和消费者都可以到同一个条件变量下去等待,但是,如果想要唤醒特定的线程的话就会很难做到
};
cpp
//main.cc
#include "BlockQueue.hpp"
#include <unistd.h>
// 生产者和消费者看到同一份资源
struct ThreadData
{
BlockQueue<int> *bq;
std::string name;
};
void *consumer(void *args)
{
ThreadData *td = static_cast<ThreadData *>(args);
while (true)
{
sleep(1);
int data = 0;
td->bq->Pop(&data);
std::cout << "消费者消费了一个数据:"<< data <<std::endl;
}
}
void *productor(void *args)
{
ThreadData *td = static_cast<ThreadData *>(args);
int data = 1;
while (true)
{
td->bq->Enqueue(data); // 不断地入队列
std::cout << "生产者生产了一个数据: " << data++ << std::endl;
}
}
int main()
{
BlockQueue<int> *bq = new BlockQueue<int>(); // 一个场所
pthread_t c, p;
ThreadData ctd = {bq, "消费者"}; // 消费者的
pthread_create(&c, nullptr, consumer, (void *)&ctd);
ThreadData ptd = {bq, "生产者"}; // 生产者的
pthread_create(&p, nullptr, productor, (void *)&ptd);
pthread_join(c, nullptr);
pthread_join(p, nullptr);
delete bq;
return 0;
}
生产者线程和消费者线程谁先运行??--- 不确定!!线程谁先启动不知道,但是生产者线程和消费者将来一定是生产者先进入临界区执行生产动作,因为一开始队列为空,即使消费者跑的快,也要等待。要演示生产消费的过程,让消费者每隔一秒消费一次,sleep(1); 生产者疯狂在生产。看到的现象应该是,生产者先生产一瞬间,将阻塞队列一瞬间充满,生产者就不能生产了,转而消费者就必须得消费了,后续的现象就是消费一个,生产一个,若实现这样的现象,生产和消费初步就实现了同步过程
运行结果:

如果让生产者慢一点点,现象就是生产一个,消费一个,生产一个,消费一个
cpp
void *consumer(void *args)
{
ThreadData *td = static_cast<ThreadData *>(args);
while (true)
{
int data = 0;
td->bq->Pop(&data);
std::cout << "消费者消费了一个数据:" << data << std::endl;
}
}
void *productor(void *args)
{
ThreadData *td = static_cast<ThreadData *>(args);
int data = 1;
while (true)
{
sleep(1);
td->bq->Enqueue(data); // 不断地入队列
std::cout << "生产者生产了一个数据: " << data++ << std::endl;
}
}
运行结果:两个线程之间出现了协同

在真实情况下,生产者和消费者的步调尽量的保持一致,即便大了也有缓冲区阻塞队列的存在,可以并不阻塞生产或消费。上面的例子是有一点极端的,但是极端可以看见同步效果。
问题1:唤醒操作太频繁
两个指标:_c_wait_num 和 _p_wait_num,衡量生产者和消费者有多少个人在等
cpp
#pragma once
#include <iostream>
#include <string>
#include <queue>
#include <pthread.h>
const static uint32_t gcap = 5; // 默认的
template <typename T> // 引入模板,里面传的数据类型不限制
class BlockQueue
{
private:
bool IsFull()
{
return _bq.size() >= _cap;
}
bool IsEmpty()
{
return _bq.empty();
}
public:
BlockQueue(uint32_t cap = gcap) : _cap(cap),_c_wait_num(0),_p_wait_num(0)
{
pthread_mutex_init(&_lock, nullptr); // 锁的初始化
pthread_cond_init(&_c_cond, nullptr); // 消费者的条件变量初始化
pthread_cond_init(&_p_cond, nullptr); // 生产者的条件变量初始化
}
// 纯输入的话,const 引用
void Enqueue(const T &in)
{
pthread_mutex_lock(&_lock);
// 要进行生产,就一定能够进行生产吗? -- 不一定!!一定要满足生产条件!!
// 队列不为满的时候才能生产
if (IsFull()) // 满了,不能再生产了
{
_p_wait_num++;
pthread_cond_wait(&_p_cond, &_lock); // 特征1:自动释放锁!! //特征2:唤醒之后,自动重新竞争锁并持有锁!!
_p_wait_num--;
}
_bq.push(in); // 完成生产
// 一定有一个数据,不一定有消费者在等的
if(_c_wait_num > 0)
pthread_cond_signal(&_c_cond); // 唤醒消费者 可放在这里 最佳推荐 放在里面
pthread_mutex_unlock(&_lock);
}
// 只有消费者最清楚,该你生产者生产了;只有生产者最清楚,该你消费者消费了
// 纯输出用指针
void Pop(T *out)
{
pthread_mutex_lock(&_lock);
// 想消费,不一定能消费,如果阻塞队列中没有数据了,就消费不了
if (IsEmpty())
{
_c_wait_num++;
// 消费者去等待,直到队列中有数据
pthread_cond_wait(&_c_cond, &_lock);
_c_wait_num--;
}
*out = _bq.front();
_bq.pop();
// 一定有一个空间,但是生产者不一定有在等,生产者有可能正在运行
if(_p_wait_num > 0)
pthread_cond_signal(&_p_cond); // 唤醒生产者
pthread_mutex_unlock(&_lock);
}
// 既是输入参数又是输出参数:用引用
~BlockQueue()
{
pthread_mutex_destroy(&_lock);
pthread_cond_destroy(&_c_cond);
pthread_cond_destroy(&_p_cond);
}
private:
std::queue<T> _bq; // blockqueue 要用到的阻塞队列
uint32_t _cap; // 容量 queue会自动扩容,所以要有一个上限的规定
pthread_mutex_t _lock;
pthread_cond_t _c_cond; // 消费者用的条件变量
pthread_cond_t _p_cond; // 生产者用的条件变量
到
int _c_wait_num; //当前消费者等待的个数,没有消费者等待,计数器为0
int _p_wait_num; //当前生产者等待的个数,有多少个生产者在等
};
此时,代码中仍然是还有bug的!!
重谈pthread_cond_wait():
如果我唤醒生产者和消费者的时候,人家本身就是醒来的,signal别人会不会影响呢?不会影响。如果你唤醒指定的线程,不管是生产者还是消费者,他本来就是醒来的,没在条件变量下去等,你唤醒它了,不影响,没在条件变量下去等,你的唤醒动作是忽略的,只有有的话才会被唤醒,高频次的触发唤醒不影响。但是这里有一个问题:
cpp
void Enqueue(const T &in)
{
pthread_mutex_lock(&_lock);
if (IsFull()) // 满了,不能再生产了
{
// pthread_cond_wait()是一个函数,
_p_wait_num++;
pthread_cond_wait(&_p_cond, &_lock); // 特征1:自动释放锁!! //特征2:唤醒之后,自动重新竞争锁并持有锁!!
_p_wait_num--;
}
// 认为队列没满 -- 可生产
_bq.push(in); // 完成生产
// 一定有一个数据,不一定有消费者在等的
if (_c_wait_num > 0)
pthread_cond_signal(&_c_cond); // 唤醒消费者 可放在这里 最佳推荐 放在里面
pthread_mutex_unlock(&_lock);
}
void Pop(T *out)
{
pthread_mutex_lock(&_lock);
// 想消费,不一定能消费,如果阻塞队列中没有数据了,就消费不了
if (IsEmpty())
{
_c_wait_num++;
// 消费者去等待,直到队列中有数据
pthread_cond_wait(&_c_cond, &_lock);
_c_wait_num--;
}
*out = _bq.front();
_bq.pop();
// 一定有一个空间,但是生产者不一定有在等,生产者有可能正在运行
if (_p_wait_num > 0)
pthread_cond_signal(&_p_cond); // 唤醒生产者
pthread_mutex_unlock(&_lock);
}
pthread_cond_wait()是一个函数:
1. 是函数,就有可能调用失败! pthread_cond_wait(&_p_cond, &_lock);的返回值是整数,如果在条件变量下去等待,等待失败了呢?失败了就立即向后运行,此时就开始 push or pop了嘛?走到 pthread_cond_wait(&_p_cond, &_lock);这里意味着生产条件是不具备的,但这个函数调用失败,导致代码继续往后运行,不就是在生产条件不满足的情况下,比如数据已经满了,还往里面生产数据此时就错了。
**2.pthread_cond_wait() 会存在伪唤醒的情况!**pthread_cond_wait(&_p_cond, &_lock); 等待的时候是成功的,又可能有其他线程直接broadcast了,其实只生产了一个数据,但是唤醒了很多线程,来进行对应的消费。其二,pthread_cond_wait(&_p_cond, &_lock); 等待的时候,对方也会给我不断地触发signal,有可能会误触发,导致条件不具备的也直接被唤醒了
判断应该由 if 改为 while,因为while本身也有判断能力,如果 pthread_cond_wait(&_p_cond, &_lock); 调用失败了,还会再判断一次条件,在判断期间一定是持有锁的情况。即便是伪唤醒的话,条件并不满足,别人把你直接唤醒了,唤醒的时候,不是继续往后走,而是继续在判断条件是否满足,while循环给了我们被唤醒重新判断的机会。
cpp
#pragma once
#include <iostream>
#include <string>
#include <queue>
#include <pthread.h>
const static uint32_t gcap = 5; // 默认的
template <typename T> // 引入模板,里面传的数据类型不限制
class BlockQueue
{
private:
bool IsFull()
{
return _bq.size() >= _cap;
}
bool IsEmpty()
{
return _bq.empty();
}
public:
BlockQueue(uint32_t cap = gcap) : _cap(cap), _c_wait_num(0), _p_wait_num(0)
{
pthread_mutex_init(&_lock, nullptr); // 锁的初始化
pthread_cond_init(&_c_cond, nullptr); // 消费者的条件变量初始化
pthread_cond_init(&_p_cond, nullptr); // 生产者的条件变量初始化
}
// 纯输入的话,const 引用
void Enqueue(const T &in)
{
pthread_mutex_lock(&_lock);
while (IsFull()) // 满了,不能再生产了
{
// pthread_cond_wait()是一个函数,
// 1. 调用失败
// 2. 伪唤醒情况
_p_wait_num++;
pthread_cond_wait(&_p_cond, &_lock); // 特征1:自动释放锁!! //特征2:唤醒之后,自动重新竞争锁并持有锁!!
_p_wait_num--;
}
// 认为队列没满 -- 可生产
_bq.push(in); // 完成生产
// 一定有一个数据,不一定有消费者在等的
if (_c_wait_num > 0)
pthread_cond_signal(&_c_cond); // 唤醒消费者 可放在这里 最佳推荐 放在里面
pthread_mutex_unlock(&_lock);
}
// 只有消费者最清楚,该你生产者生产了;只有生产者最清楚,该你消费者消费了
// 纯输出用指针
void Pop(T *out)
{
pthread_mutex_lock(&_lock);
// 想消费,不一定能消费,如果阻塞队列中没有数据了,就消费不了
while (IsEmpty())
{
_c_wait_num++;
// 消费者去等待,直到队列中有数据
pthread_cond_wait(&_c_cond, &_lock);
_c_wait_num--;
}
*out = _bq.front();
_bq.pop();
// 一定有一个空间,但是生产者不一定有在等,生产者有可能正在运行
if (_p_wait_num > 0)
pthread_cond_signal(&_p_cond); // 唤醒生产者
pthread_mutex_unlock(&_lock);
}
// 既是输入参数又是输出参数:用引用
~BlockQueue()
{
pthread_mutex_destroy(&_lock);
pthread_cond_destroy(&_c_cond);
pthread_cond_destroy(&_p_cond);
}
private:
std::queue<T> _bq; // blockqueue 要用到的阻塞队列
uint32_t _cap; // 容量 queue会自动扩容,所以要有一个上限的规定
pthread_mutex_t _lock;
pthread_cond_t _c_cond; // 消费者用的条件变量
pthread_cond_t _p_cond; // 生产者用的条件变量
int _c_wait_num; // 当前消费者等待的个数,没有消费者等待,计数器为0
int _p_wait_num; // 当前生产者等待的个数,有多少个生产者在等
};
2.7 生产者和消费者的周边问题

2.7.1 多生产者多消费者的代码:
当前是维护了生产者和消费者的互斥关系,同步关系,需要新增的就是生产者之间的互斥关系,消费者之间的互斥关系。用以上的代码如果有多个生产者多个消费者,天然也是互斥的,彼此之间的互斥关系用一把锁就解决了。直接平滑过渡:
cpp
#include "BlockQueue.hpp"
#include <unistd.h>
// 生产者和消费者看到同一份资源
struct ThreadData
{
BlockQueue<int> *bq;
std::string name;
};
void *consumer(void *args)
{
ThreadData *td = static_cast<ThreadData *>(args);
while (true)
{
sleep(1);
int data = 0;
td->bq->Pop(&data);
std::cout << "消费者消费了一个数据:" << data << std::endl;
}
}
void *productor(void *args)
{
ThreadData *td = static_cast<ThreadData *>(args);
int data = 1;
while (true)
{
td->bq->Enqueue(data); // 不断地入队列
std::cout << "生产者生产了一个数据: " << data++ << std::endl;
}
}
int main()
{
BlockQueue<int> *bq = new BlockQueue<int>(); // 一个场所
pthread_t c[2], p[3];
ThreadData ctd = {bq, "消费者"}; // 消费者的
pthread_create(c+0, nullptr, consumer, (void *)&ctd);
pthread_create(c+1, nullptr, consumer, (void *)&ctd);
ThreadData ptd = {bq, "生产者"}; // 生产者的
pthread_create(p+0, nullptr, productor, (void *)&ptd);
pthread_create(p+1, nullptr, productor, (void *)&ptd);
pthread_create(p+2, nullptr, productor, (void *)&ptd);
pthread_join(c[0], nullptr);
pthread_join(c[1], nullptr);
pthread_join(p[0], nullptr);
pthread_join(p[1], nullptr);
pthread_join(p[2], nullptr);
delete bq;
return 0;
}
运行结果:

2.7.2 高效体现在哪里?生产和消费的过程都是串行的呀?

为什么是高效的呢?事实是在生产和消费的过程,这个过程就是串行的!整体使用阻塞队列的场景当中,他就应该是串行的!!因为不串行就会引发并发的问题,因为你是临界资源呀!!
高效如何体现:
cpp
void *consumer(void *args)
{
ThreadData *td = static_cast<ThreadData *>(args);
while (true)
{
sleep(1);
int data = 0;
td->bq->Pop(&data);
std::cout << "消费者消费了一个数据:" << data << std::endl;
}
}
void *productor(void *args)
{
ThreadData *td = static_cast<ThreadData *>(args);
int data = 1;
while (true)
{
td->bq->Enqueue(data); // 不断地入队列
std::cout << "生产者生产了一个数据: " << data++ << std::endl;
}
}
作为生产者来讲:
1. 可以将数据生产到对应的阻塞队列中
2. 生产者你的数据从哪里来?--- 从网络中来!!
作为消费者来讲:
1. 从交易场所中,获取数据或者任务
2. 对数据做加工,或者执行任务!

- 生产者消费者模型的松耦合关系体现在:获取数据来源 和 处理数据或者任务 两端。中间的阻塞队列明显是紧耦合关系!!不影响,这是必然的。
- 生产者消费者模型支持忙闲不均体现在:数据生产的太多了暂时不生产了,但是消费者可以疯狂的消费,拿任务是串行的但是执行任务是并发的。
- 基于以上两点就可以减少生产成本了,体现在:线程在需要串行时串行,不需要串行就并发。
- 所以不能只盯着中间的同步互斥那一块去谈,因为它本身的效率就不高!!
2.7.3 生产和消费,就是传递数据吗?
往阻塞队列中写了一个整数,把整数从一个线程写到另一个线程,这有什么意义呢?
- 线程间通信的时候,确实可以传递整数,浮点数,字符串等等
- 传递类对象可以吗?传递函数可以吗??我可以给另一个线程派发任务吗?------ 都可以
- 既可以传递数据,也可以派发任务!!
代码调优:依旧使用单生产单消费
cpp
#pragma once
#include <iostream>
#include <string>
#include <queue>
#include <pthread.h>
const static uint32_t gcap = 5; // 默认的
template <typename T> // 引入模板,里面传的数据类型不限制
class BlockQueue
{
private:
bool IsFull()
{
return _bq.size() >= _cap;
}
bool IsEmpty()
{
return _bq.empty();
}
public:
BlockQueue(uint32_t cap = gcap) : _cap(cap), _c_wait_num(0), _p_wait_num(0)
{
pthread_mutex_init(&_lock, nullptr); // 锁的初始化
pthread_cond_init(&_c_cond, nullptr); // 消费者的条件变量初始化
pthread_cond_init(&_p_cond, nullptr); // 生产者的条件变量初始化
}
// 纯输入的话,const 引用
void Enqueue(const T &in)
{
pthread_mutex_lock(&_lock);
while (IsFull()) // 满了,不能再生产了
{
_p_wait_num++;
pthread_cond_wait(&_p_cond, &_lock); // 特征1:自动释放锁!! //特征2:唤醒之后,自动重新竞争锁并持有锁!!
_p_wait_num--;
}
_bq.push(in); // 完成生产
if (_c_wait_num > 0)
pthread_cond_signal(&_c_cond); // 唤醒消费者 可放在这里 最佳推荐 放在里面
pthread_mutex_unlock(&_lock);
}
// 纯输出用指针
void Pop(T *out)
{
pthread_mutex_lock(&_lock);
while (IsEmpty())
{
_c_wait_num++;
// 消费者去等待,直到队列中有数据
pthread_cond_wait(&_c_cond, &_lock);
_c_wait_num--;
}
*out = _bq.front();
_bq.pop();
if (_p_wait_num > 0)
pthread_cond_signal(&_p_cond); // 唤醒生产者
pthread_mutex_unlock(&_lock);
}
// 既是输入参数又是输出参数:用引用
~BlockQueue()
{
pthread_mutex_destroy(&_lock);
pthread_cond_destroy(&_c_cond);
pthread_cond_destroy(&_p_cond);
}
private:
// 队列、容量,都是属于类类的一种全局数据 ------ 临界资源
std::queue<T> _bq; // blockqueue 要用到的阻塞队列
uint32_t _cap; // 容量 queue会自动扩容,所以要有一个上限的规定
pthread_mutex_t _lock;
pthread_cond_t _c_cond; // 消费者用的条件变量
pthread_cond_t _p_cond; // 生产者用的条件变量
int _c_wait_num; // 当前消费者等待的个数,没有消费者等待,计数器为0
int _p_wait_num; // 当前生产者等待的个数,有多少个生产者在等
};
cpp
//Task.hpp
#pragma once
#include <iostream>
class Task
{
public:
Task()
{
}
Task(int x, int y) : a(x), b(y)
{
}
void Execute()
{
result = a + b;
}
void operator()()
{
Execute();
}
void Print()
{
std::cout << a << " + " << b << " = " << result << std::endl;
}
private:
int a;
int b;
int result;
};
cpp
#include "BlockQueue.hpp"
#include "Task.hpp"
#include <unistd.h>
// 生产者和消费者看到同一份资源
struct ThreadData
{
BlockQueue<Task> *bq;
std::string name;
};
void *consumer(void *args)
{
// 1. 从交易场所中,获取数据或者任务
// 2. 对数据做加工,或者执行任务!
ThreadData *td = static_cast<ThreadData *>(args);
while (true)
{
sleep(1);
Task t;
// 1. 从交易场所中,获取数据或者任务
td->bq->Pop(&t); //从公有的部分拿到私有部分
// 2. 对数据做加工,或者执行任务!
t();
t.Print();
}
}
void *productor(void *args)
{
ThreadData *td = static_cast<ThreadData *>(args);
int data = 1;
while (true)
{
// 1. 生产者你的数据从哪里来?
// 从网络中 or 其他主机中来的!!
int x = data;
int y = data + 1;
Task t(x,y);
data++;
// 2. 作为生产者来讲,可以将数据生产到对应的阻塞队列中
td->bq->Enqueue(t); // 不断地入队列
std::cout << "生产者生产了一个任务"<< std::endl;
}
}
int main()
{
// 既可以传递数据,也可以派发任务!!
// 线程间通信的时候,确实可以传递整数,浮点数,字符串等等
// 传递类对象可以吗?传递函数可以吗??我可以给另一个线程派发任务吗?
BlockQueue<Task> *bq = new BlockQueue<Task>(); // 一个场所
pthread_t c, p;
ThreadData ctd = {bq, "消费者"}; // 消费者的
pthread_create(&c, nullptr, consumer, (void *)&ctd);
ThreadData ptd = {bq, "生产者"}; // 生产者的
pthread_create(&p, nullptr, productor, (void *)&ptd);
pthread_join(c, nullptr);
pthread_join(p, nullptr);
// pthread_t c[2], p[3];
// ThreadData ctd = {bq, "消费者"}; // 消费者的
// pthread_create(c + 0, nullptr, consumer, (void *)&ctd);
// pthread_create(c + 1, nullptr, consumer, (void *)&ctd);
// ThreadData ptd = {bq, "生产者"}; // 生产者的
// pthread_create(p + 0, nullptr, productor, (void *)&ptd);
// pthread_create(p + 1, nullptr, productor, (void *)&ptd);
// pthread_create(p + 2, nullptr, productor, (void *)&ptd);
// pthread_join(c[0], nullptr);
// pthread_join(c[1], nullptr);
// pthread_join(p[0], nullptr);
// pthread_join(p[1], nullptr);
// pthread_join(p[2], nullptr);
delete bq;
return 0;
}
运行结果:

之后你想让线程做什么,直接改任务就行了,在代码层面上也解耦了!!!
