如何看待之前学习的单进程?具有一个线程执行流的进程
Linux中的执行流统称为轻量级进程(这里既不是进程,也不是线程,而是执行流)
cache : 高速缓存
局部性原理:当前正在访问的资源,它附近的资源有很大概率被访问到
os先把当前的代码/其他资源加载到cache(高速缓存),我们cpu来拿取资源,是直接从cache里面拿资源的,如果拿到了想要的资源,就称为命中了,继续执行代码即可。如果没有拿到想要的资源,就称为未命中,cpu就需要再去内存中加载数据,从内存中拿取数据,也是先缓存到cache里面,然后cpu再从cache里面读取
共享资源并不一定是全局的!(main函数内部的资源,可以被新线程使用的,main函数内部资源是共享资源!)
一般而言
创建线程数与cpu的核数有关(cpu的核数 —— cpu内部有多少个运算器)
创建进程数和cpu个数有关(cpu的个数 —— 有独立的多个cpu)
cpu由运算器,控制器和寄存器构成(存储器不是cpu的组成部分)
电脑上面显示的cpu多核分两种:一种是真多核,一种是伪多核
真多核:有多个运算器 —— 多个寄存器 —— 一个控制器
伪多核:有多个运算器 —— 一套寄存器(并没有多数的寄存器) —— 一个控制器
我们本节讲的多线程内容是在linux平台下面的线程内容,其他平台由于底层实现多线程的方式不同,这里就不深讲了
补充注意:本节上半内容代码的cout打印代码建议大家换成printf函数!因为cout不是原子性的!打印会出现乱序!printf通常不是原子性的,但是将输出打印到单个流的时候是原子操作(如果两个线程使用printf打印到两个流,那么还是会出现乱序的情况)
要么就加上cout.flush()函数
要想一劳永逸就需要本节后面锁的知识了!
1、地址空间是进程能看到的资源窗口(内核区,栈区,堆区…)
2、页表(页表+MMU内存管理单元)决定进程真正拥有资源的情况(每一个进程都认为自己独有4GB空间,可真正拥有多少物理资源由页表映射关系决定)
3、合理对 地址空间+页表 进行资源划分,我们就可以对一个进程的所有资源进行分类(进程看到的资源通过地址空间将虚拟内存划分为:内核区,栈区,堆区…通过页表映射到不同的物理内存)
系统调用接口开始就会有汇编指令帮助我们陷入内核,这个时候我们的U/K权限就自动切换为K权限,然后就可以通过物理地址找到os,开始执行系统调用了
str先通过虚拟地址找到了物理地址,但是后续的RWX权限str只有R权限,而*str = 'H'
是W权限,所以硬件会报错,os识别到这个硬件报错就是11号信号段错误,然后向进程发送信号,进程收到信号之后,默认动作就是终止进程
就算os把页目录映射的页表的全部都加载进来,其实也不大,因为我们虚拟地址最后的12个比特位是没有进行映射的,它是页内偏移量
线程是进程内的一个执行流! 可能很多人看到这里就很懵了,这是很多书上的说法,那么为什么书上要这么说呢?因为操作系统太宏观了,有许多的版本(不同的平台底层实现多线程的方法不同!)。而线程是进程内部都一个执行流这句话放到所有的操作系统上面都是对的!
所以,我们具体就只具体来谈谈linux中的多线程。但是,linux中的多线程也是要满足:线程是进程内的一个执行流这句话特点
如何看待虚拟内存呢? : 虚拟内存决定了进程能够看到的“资源” (进程 :人,虚拟内存 :窗口,资源 : 窗外风景)
我们以前进fork子进程的时候,子进程要拷贝父进程的mm_struct(虚拟内存),页表,以及物理内存写入时要发生写时拷贝
而我们今天可以创建一批“进程”,都指向同一个mm_struct(虚拟内存)
理解 : 进程所拥有的资源是可以通过 进程地址空间+页表 将一部分资源划分给特定的线程的!所以,单个线程要比之前进程的执行力度更细,就像以前的进程一样,fork子进程之后,通过判断pid是否为0,就可以把一段代码块交给子进程执行!
如果我们os真的要设计“线程”这样的概念,os未来要不要对线程进行管理呢? -> 肯定要管理的,那么如何管理呢? -> 先描述,再组织 -> 一定要为线程设计专门的数据结构来表示线程对象 -> TCB(线程控制块),本质上也是一个struct结构体 -> 再组织 : PCB的内部有一张链表,通过链表将TCB对象一个个链接起来。当我们执行代码进行调度时,先找到进程,然后在进程内部跳到指定的线程再进行调度
那么什么平台采用的就是上面对线程管理的方法呢? -> windows平台是这么做的!
但是,我们仔细想想一个线程被创建的根本目的是什么呢? -> 是为了被执行,然后被调度 -> 被调度就要有对应的:id,状态,优先级,上下文,栈… 单纯从线程的角度来看,线程和进程有很多地方是重叠的!
所以,linux工程师,不想给“线程”专门设计对应的数据结构!而是直接复用PCB!用PCB来表示linux内部的“线程”
线程是进程内的一个执行流 —— 线程在进程内部运行 —— 线程在进程的地址空间内运行!拥有该进程的一部分资源!
提出问题:
1、今天学习了线程概念之后,什么叫进程呢?
承担分配系统资源的基本实体(单位)(系统进行资源分配的最小单位) —— 简单来说就是 : 进程要占用系统很多的资源,比如IO资源
2、linux中,什么叫做线程呢?
线程是 : cpu调度的最小单位
3、如何看待我们之前学习进程时,对应的进程概念呢?(也就是说:我们以前讲的进程内容是不是有问题呢?)和今天讲的冲突吗?
以前的进程概念 : 承担分配系统资源的实体(单位),只不过进程内部只有一个执行流!而今天 : 一个进程内部有多个执行流
也就是说cpu执行的PCB都是轻量级进程了!哪怕进程只有一个执行流,也是一个轻量级进程
结论:
1、linux内核中,严格意义上来讲是没有真正意义上的线程的 —— linux是用进程PCB来模拟线程的,是一种完全属于这就的一套线程方案
2、站在cpu的视角,每一个pcb都可以叫做轻量级进程(因为对比其他平台,和我们以前的进程,今天我们学习的PCB(task_struct),可能是进程内部的线程,也可能就是一个进程执行流,所以无论怎么看,今天的pcb都<=以前的pcb,我们就称pcb为轻量级进程)
3、linux中,进程是 : 承担分配系统资源的基本实体/单位;线程是 : cpu调度的最小单位
4、进程用来整体申请资源,线程用来伸手向进程要资源(线程未来malloc等获取资源,本质是进程在获取资源,因为线程是进程内一部分)
5、linux中没有真正意义上的线程
6、这么做的好处是什么? —— 简单,维护成本大大降低,可靠高效!
os只认线程,程序员也只认线程(都不认你linux所谓的轻量级进程) —— 所以,linux无法直接提供创建线程的系统调用接口!而只能提供创建轻量级进程的接口!
举例:
家庭:进程
家庭成员:线程
社会:os
只认线程,程序员也只认线程(都不认你linux所谓的轻量级进程)
linux无法直接提供创建线程的系统调用接口!而只能提供创建轻量级进程的接口!
所以,为了方便用户创建线程,程序员编写一个用户级线程库(原生线程库:pthread库),它位于我们用户层和系统调用接口之间,我们使用线程库提供的接口,该库就会将我们对线程的操作内部转换为对轻量级进程的操作
pthread线程库 :在任何linux操作系统下面都有 —— 原生线程库
这里提个问题:
pthread库是这么和linux中的轻量级进程建立连接的呢?(也就是怎么把我们对线程的操作,在内部转换为对轻量级进程的操作?)
#include
#include
#include
#include
using std::cin;
using std::cout;
using std::endl;
// 这里是 —— 新线程/从线程
void *pthread_handler(void *arr)
{
while (1)
{
cout << "我是新线程!" << endl;
sleep(1);
}
}
int main()
{
pthread_t tid;
int n = pthread_create(&tid, nullptr, pthread_handler, (void *)"pthread one");
assert(n == 0);
(void)n;
// 这里到后面是 —— 主线程!
while (1)
{
cout << "我是主线程!";
sleep(1);
}
return 0;
}
cpu进行调度的时候,是通过LWP
这个id,来表示特定的一个执行流的,而不是pid!
而我们以前使用pid也是没有问题的
因为以前,我们只有一个进程(一个执行流),所以pid等价于lwp
我们上面说了pthread_create的第4个参数是作用于第3个参数的,我们来验证一下:
void *pthread_handler(void *arr)
{
const char *ptr = (const char *)arr;
while (1)
{
cout << "我是新线程!name :" << ptr << endl;
sleep(1);
}
}
我们将tid转换为16进制然后打印出来
while (1)
{
char tidbuffer[1024];
snprintf(tidbuffer,sizeof(tidbuffer),"0x%x",tid);//格式化函数 —— 将tid转化为0x%x十六进制的类型,放到tidbuffer里面
cout << "我是主线程!我的tid是 :" << tidbuffer << endl;
sleep(1);
fflush(stdout);
}
什么tid这么长的一串数字看起来很像一个地址,其实它就是一个地址,不过我们目前还讲不清楚这个地址到底是什么,后面才能讲清楚!
1、线程一旦被创建,几乎所有的资源都是被所有线程所共享的!
#include
#include
#include
#include
using std::cin;
using std::cout;
using std::endl;
int g_val = 0;
// 这里是 —— 新线程/从线程
void *pthread_handler(void *arr)
{
const char *name = (const char *)arr;
while (1)
{
// cout << "我是新线程!name :" << ptr << endl;
cout << "我是新线程, 我正在运行! name: " << name << " : " << g_val++ << " &g_val : " << &g_val << endl;
sleep(1);
fflush(stdout);
}
}
int main()
{
pthread_t tid;
int n = pthread_create(&tid, nullptr, pthread_handler, (void *)"pthread one");
assert(n == 0);
(void)n;
// 这里到后面是 —— 主线程!
while (1)
{
char tidbuffer[1024];
snprintf(tidbuffer, sizeof(tidbuffer), "0x%x", tid); // 格式化函数 —— 将tid转化为0x%x十六进制的类型,放到tidbuffer里面
// cout << "我是主线程!我的tid是 :" << tidbuffer << endl;
cout << "我是主线程, 我正在运行!, 我创建出来的线程的tid: " << tidbuffer << " : " << g_val << " &g_val : " << &g_val << endl;
sleep(1);
fflush(stdout);
}
return 0;
}
2、线程也有自己的私有资源,那么有哪一些资源是线程私有的呢?(面试)
1、pcb的属性私有(线程要被调度,所以它的优先级,id等等都要私有)
2、线程的上下文结构私有(线程也可能会被切换,如果线程没有执行完,就要先保存该线程的上下文结构,方便恢复重新执行该线程)
3、每一个线程都要有独立的栈结构(线程都有对应的代码,执行时都要形成对应的局部变量,而这些局部变量都是存在栈上面的)
线程A在堆区上new了一块空间,如果将这个堆空间地址保存在全局变量指针中,任何线程都能够访问该堆空间
只不过如果线程A new空间的地址没有保存在全局变量中,那么该地址就在该线程的栈上面,而栈是独立的,使用就给人一种感觉堆空间是该线程私有的!
线程内的局部变量也是同理,只要把线程内的局部变量的地址给给全局变量指针,那么其他线程就都能通过全局变量指针拿到该线程的局部变量
全局变量就更不用说了,任何线程可以直接拿到!
这里再提出一个问题:我们在怎么保证每一个线程都有自己的栈结构呢?
3、与进程之间的切换相比,线程之间的切换要os做的工作要少得多
进程切换 : 切换页表 —— 切换虚拟地址空间 —— 切换上下文 —— 切换pcb
线程切换:切换上下文 —— 切换pcb
线程切换cache(高速缓存)不用太更新,而进程切换cache要全部更新(主要的工作)
cpu内部有一个cache(高速缓存),它比cpu其他硬件效率慢一点,但是比内存的效率快很多
局部性原理:当前正在访问的资源,它附近的资源有很大概率被访问到
os先把当前的代码/其他资源加载到cache(高速缓存),我们cpu来拿取资源,是直接从cache里面拿资源的,如果拿到了想要的资源,就称为命中了,继续执行代码即可。如果没有拿到想要的资源,就称为未命中,cpu就需要再去内存中加载数据,从内存中拿取数据,也是先缓存到cache里面,然后cpu再从cache里面读取
所以,cache里面的热点数据的加载,才是线程为什么切换比进程切换做的工作要少的原因!
4、计算密集型应用和I/O密集型应用
计算密集型应用:主要是进程/线程使用的资源是 :cpu的资源(加密,解密,算法等等 —— 打包压缩,执行打包和压缩对应的算法)
I/O密集型应用:主要是进程/线程使用的资源是 :外设的资源(访问磁盘,访问网络,显示器等等 —— 抖音,迅雷)
5、多线程健壮性问题
简单来说:多个线程共享大部分数据,而一个线程在使用某个数据的时候,另一个线程可能正在对该数据进行修改 —— 一个线程影响到了另外一个线程(缺乏访问控制)
我们先来看看正常情况的代码:
#include
#include
#include
#include
using namespace std;
void *start_routine(void *argv)
{
string name = static_cast<const char *>(argv);
while (1)
{
cout << "thread name is : " << name << endl;
sleep(1);
}
}
int main()
{
pthread_t id;
pthread_create(&id, nullptr, start_routine, (void *)"thread 1"); // 最后一个参数要强转
while (1)
{
cout << "thread name is :main thread " << endl;
sleep(1);
}
return 0;
}
所以,一个线程出了异常会影响另一个线程!
而这种线程就叫做健壮性或者鲁棒性较差!
为什么会出现一个线程出了异常会影响另一个线程的情况呢?
以前,在我们看来(一个进程内部产生的线程),因为该线程出现异常被os捕捉到了(硬件异常,软件条件,终端按键,系统调用等方法产生信号),os捕捉到异常之后,通过pid给每一个线程/进程发送信号(不是LWP!)。也就是说,每一个pid相同的轻量级进程都收到了os发送的信号(线程的pid都是相同的)。然后每一个线程执行信号的默认动作,终止执行流,那么所有的线程就都被终止了,进程也就被直接终止了!
今天,在我们看来(一个进程内部产生的线程),线程是进程内的一个执行流,也就是进程内的一部分!线程做任何事情就代表进程在做该事情,那么一旦线程出问题了,进程也就出问题了,线程和进程应该是一个整体!
一个线程出问题了,那么线程所属进程就出问题了(线程和进程是一个整体),进程被创建os需要分配对应的资源,而进程被销毁,os要释放进程的资源。而线程的资源是有进程来分配的!所有进程被终止了,线程也就自动销毁了
1、在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”
2、一切进程至少都有一个执行线程
3、线程在进程内部运行,本质是在进程地址空间内运行
4、在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化
5、透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流
1、创建一个新线程的代价要比创建一个新进程小得多(对比于进程)
2、与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多(对比于进程)
3、线程占用的资源要比进程少很多(对比于进程)
4、能充分利用多处理器的可并行数量(—— 从这里开始下面的优点进程也有)
5、在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
6、计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
7、I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。
1、性能损失
一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
2、健壮性降低
编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。(一个线程异常了可能会影响到其他线程)
3、缺乏访问控制
进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
4、编程难度提高
编写与调试一个多线程程序比单线程程序困难得多
1、单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃
2、线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出
1、合理的使用多线程,能提高CPU密集型程序的执行效率
2、合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)
进程是资源分配的基本单位
线程是调度的基本单位
线程共享进程数据,但也拥有自己的一部分数据:
1、 线程ID
2、一组寄存器
3、栈
4、errno
5、信号屏蔽字
6、调度优先级
进程的多个线程共享 同一地址空间,因此Text Segment(代码段)、Data Segment(数据段)都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:
1、文件描述符表
2、每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
3、当前工作目录
4、用户id和组id
我们前面说了,linux只提供了创建轻量级进程的接口,那么这个轻量级进程接口是什么呢?
fork和vfork等函数的底层调用的就是这个clone函数!(ptrhead_create -> clone)
我们不用clone这个函数,而是对应的原生线程库在用这个函数
1、与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头的
2、要使用这些函数库,要通过引入头文
3、链接这些线程函数库时要使用编译器命令的“-lpthread”选项
功能:创建一个新的线程
原型
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void * (*start_routine)(void*), void *arg);
参数
thread:返回线程ID
attr:设置线程的属性,attr为NULL表示使用默认属性
start_routine:是个函数地址,线程启动后要执行的函数
arg:传给线程启动函数的参数
返回值:成功返回0;失败返回错误码
传统的一些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误。
pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做)。而是将错误代码通过返回值返回
pthreads同样也提供了线程内的errno变量,以支持其它使用errno的代码。对于pthreads函数的错误,建议通过返回值业判定,因为读取返回值要比读取线程内的errno变量的开销更小
绝大部分线程的错误原因是由返回值来告诉我们的
线程ID及进程地址空间布局:
1、pthread_ create函数会产生一个线程ID,存放在第一个参数指向的地址中。该线程ID和前面说的线程ID不是一回事。
2、前面讲的线程ID属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程。
3、pthread_ create函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID,属于NPTL线程库的范畴。线程库的后续操作,就是根据该线程ID来操作线程的。
4、线程库NPTL提供了pthread_ self函数,可以获得线程自身的ID:
pthread_t pthread_self(void);
pthread_t 到底是什么类型呢?取决于实现。对于Linux目前实现的NPTL实现而言,pthread_t类型的线程ID,本质就是一个进程地址空间上的一个地址
我们下面的3-10会讲
一次性创建10个线程:
#include
#include
#include
#include
#include
using namespace std;
void *start_routine(void *argv)
{
string name = static_cast<const char *>(argv);
while (1)
{
cout << "thread name is : " << name << endl;
sleep(1);
}
}
int main()
{
vector<pthread_t> vp;
#define NUM 10
for (size_t i = 0; i < NUM; ++i)
{
pthread_t tid;
pthread_create(&tid, nullptr, start_routine, (void *)"thread 1");
}
while (1)
{
cout << "thread name is :main thread " << endl;
sleep(1);
}
return 0;
}
#include
#include
#include
#include
#include
using namespace std;
void *start_routine(void *argv)
{
string name = static_cast<const char *>(argv);
while (true)
{
cout << "thread name is : " << name << endl;
sleep(1);
}
}
int main()
{
vector<pthread_t> vp;
#define NUM 10
for (size_t i = 0; i < NUM; ++i)
{
pthread_t tid;
char namebuffer[64];
snprintf(namebuffer, sizeof(namebuffer), "%s:%d", "thread", i);
// pthread_create(&tid, nullptr, start_routine, (void *)"thread 1");
pthread_create(&tid, nullptr, start_routine, namebuffer);
sleep(1);
}
while (true)
{
cout << "thread name is :main thread " << endl;
sleep(1);
}
return 0;
}
改进:
#include
#include
#include
#include
#include
using namespace std;
class ThreadDate
{
public:
pthread_t tid;
char namebuffer[64];
};
void *start_routine(void *argv) // 拷贝td,形参是实参的一份临时拷贝
{
ThreadDate *name = static_cast<ThreadDate *>(argv);
int cnt = 10;
while (cnt)
{
cout << "thread name is : " << name->namebuffer << "cnt:" << cnt-- << endl;
sleep(1);
}
delete name;
return nullptr;
}
int main()
{
vector<pthread_t> vp;
#define NUM 10
for (size_t i = 0; i < NUM; ++i)
{
ThreadDate *td = new ThreadDate();
snprintf(td->namebuffer, sizeof(td->namebuffer), "%s:%d", "thread", i);
pthread_create(&td->tid, nullptr, start_routine, td); // 这里传td,
}
while (true)
{
cout << "main thread " << endl;
sleep(1);
}
return 0;
}
所以,start_routine函数要被10个线程调用!这个函数现在就是重入状态!是一个可重入函数
在函数内定义的变量,都叫做局部变量,具有临时性 —— 在多线程的情况下也没有问题 —— 每一个线程都有独立的栈结构!
线程函数调用完毕,return的时候就终止了!
需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了
pthread_exit函数
线程是可以被cancel取消的!
前提:线程要运行起来了,才能被取消!
一个线程被取消,它的退出码就是-1
线程也是要被等待的!—— 如果不等待,会造成类似于僵尸进程的问题 —— 内存泄漏!
为什么要线程等待:
1、获取新线程/从线程的退出信息(可以不关心,但是必须要回收线程资源)
2、创建新的线程不会复用刚才退出线程的地址空间,回收新线程对应的pcb等内核资源,防止泄露
功能:等待线程结束
原型
int pthread_join(pthread_t thread, void **value_ptr);
参数
thread:线程ID
value_ptr:它指向一个指针,后者指向线程的返回值
返回值:成功返回0;失败返回错误码
调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,总结如下:
- 如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。
- 如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数PTHREAD_ CANCELED。
- 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数。
- 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数。
#include
#include
#include
#include
#include
#include
#include
using namespace std;
class ThreadDate
{
public:
pthread_t tid;
char namebuffer[64];
};
void *start_routine(void *argv) // 拷贝td,形参是实参的一份临时拷贝
{
// sleep(1);
ThreadDate *td = static_cast<ThreadDate *>(argv);
int cnt = 10;
while (cnt)
{
// cout << "thread name is : " << td->namebuffer << "cnt:" << cnt-- << endl;
cout << "cnt :" << cnt-- << "&cnt :" << &cnt << endl;
sleep(1);
// return nullptr;
// pthread_exit(nullptr);
}
//delete td;
return nullptr;
}
int main()
{
vector<ThreadDate *> vp;
#define NUM 10
for (size_t i = 0; i < NUM; ++i)
{
ThreadDate *td = new ThreadDate();
snprintf(td->namebuffer, sizeof(td->namebuffer), "%s:%d", "thread", i);
pthread_create(&td->tid, nullptr, start_routine, td); // 这里传td
vp.push_back(td);
}
for (auto &iter : vp)
{
cout << "create thread: " << iter->namebuffer << " : " << iter->tid << " suceesss" << endl;
}
for (auto &iter : vp)
{
int n = pthread_join(iter->tid, nullptr);
assert(n == 0);
cout << "join:" << iter->namebuffer << " suceesss" << endl;
delete iter;
}
cout << "join end" << endl;
return 0;
}
所以,线程是可以等待的,等待的时候是join等待的 —— 阻塞式等待
一句话 : pthread_join函数的第二个参数就是线程执行函数的返回结果!
因为用了pthread库,所以我们不能直接从pthread库里面拿到返回值,要使用pthread_join函数来拿返回值!
void * ret =nullptr void * 106
void ** p = &ret
*p = 106 —— *p就是ret
return (void*)106 ,将106强转为指针类型了,而106就是一个指针地址!(4字节数据填到8字节地址中)。
pthread库里面存放void * 106这个地址
ret是一个void的地址,里面存放一个8字节的地址
&ret,就是取出一个指针变量的地址,(&ret) —— 就拿到了ret这个指针变量,也就是void * ret,然后将void * 106赋值给ret,这样我们就将线程执行函数的返回值拿到pthread_join函数的第二个参数里面了!
为什么没有见到线程退出时候对应的退出信号呢?
线程出异常,收到信号,整个进程都会退出!
pthread_join默认函数会调用成功。不考虑异常问题,异常问题是进程考虑的!
#include
#include
#include
using std::cout;
using std::endl;
void thread_run()
{
while (true)
{
cout << "我是新线程..." << endl;
sleep(1);
}
}
int main()
{
std::thread t1(thread_run);
while (true)
{
cout << "我是主线程..." << endl;
sleep(1);
}
t1.join();
return 0;
}
任何语言,在linux中如果要实现多线程,必定要是用pthread库。如何看待C++11中的多线程呢??C++11 的多线程,在Linux环境中,本质是对pthread库的封装
pthread线程库意义:给语言级别的线程提供底层的接口支持
我们上面学到了主线程调用pthread_join函数进行阻塞式等待从线程,而线程是没有非阻塞等待的!
默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏
如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源
int pthread_detach(pthread_t thread);
可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离:
pthread_detach(pthread_self());
joinable和分离是冲突的,一个线程不能既是joinable又是分离的
我们来看看正常情况下的代码:
#include
#include
#include
#include
#include
using std::cout;
using std::endl;
void *start_toutine(void *args)
{
std::string name = static_cast<const char *>(args);
while (1)
{
//这里和上面不同,采用printf向一个流(stdout)打印结果是原子性的!
printf("%s\n", name.c_str());//字符串要记得转换称为c类型的,头文件cstring
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, start_toutine, (void *)"thread 1");
pthread_join(tid, nullptr);//进行等待
return 0;
}
我们线程可以通过主线程分离指定线程,也可以新线程分离自己!不需要进行join等待
那么我们对上面的代码进行加工,打印出线程的tid
#include
#include
#include
#include
#include
#include
using std::cout;
using std::endl;
std::string ThreadId(const pthread_t &pthtred_id)
{
char buffer[128];
snprintf(buffer, sizeof(buffer), "0x%x", pthread_self());
return buffer;
}
void *start_toutine(void *args)
{
std::string name = static_cast<const char *>(args);
while (1)
{
// 这里pthread_self获取到的id必须和下面主函数ThreadId获取到的id是一样的!
printf("%s running... %s\n", name.c_str(), ThreadId(pthread_self()).c_str());
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, start_toutine, (void *)"thread 1");
printf("main thread running... new thread id : %s\n", ThreadId(tid).c_str());
pthread_join(tid, nullptr);
return 0;
}
#include
#include
#include
#include
#include
#include
using std::cout;
using std::endl;
std::string ThreadId(const pthread_t &pthtred_id)
{
char buffer[128];
snprintf(buffer, sizeof(buffer), "0x%x", pthread_self());
return buffer;
}
void *start_toutine(void *args)
{
std::string name = static_cast<const char *>(args);
// pthread_detach(pthread_self()); // 线程将自己分离!
int cnt = 5;
while (cnt--)//先不分离线程,先检查5s之后线程等待的值是不是0
{
printf("%s running... %s\n", name.c_str(), ThreadId(pthread_self()).c_str());
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, start_toutine, (void *)"thread 1");
std::string main_tid = ThreadId(pthread_self());
printf("main thread running... new thread id : %s —— main threead id : %s\n", ThreadId(tid).c_str(), main_tid.c_str());
// 一个线程创建出来默认是joinable的,如果设置了分离状态,就不能再对该线程进行等待了
int n = pthread_join(tid, nullptr);
printf("result : %d : %s\n", n, strerror(n));//5s之后n应该是0
return 0;
}
线程分离代码:
#include
#include
#include
#include
#include
#include
using std::cout;
using std::endl;
std::string ThreadId(const pthread_t &pthtred_id)
{
char buffer[128];
snprintf(buffer, sizeof(buffer), "0x%x", pthread_self());
return buffer;
}
void *start_toutine(void *args)
{
std::string name = static_cast<const char *>(args);
int cnt = 5;
while (cnt--)
{
printf("%s running... %s\n", name.c_str(), ThreadId(pthread_self()).c_str());
sleep(1);
}
return nullptr;
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, start_toutine, (void *)"thread 1");
std::string main_tid = ThreadId(pthread_self());
pthread_detach(tid);
printf("main thread running... new thread id : %s —— main threead id : %s\n", ThreadId(tid).c_str(), main_tid.c_str());
while (1)
{
//主线程做自己的事情...
printf("result : %d : %s\n", n, strerror(n));
sleep(1);
}
return 0;
}
我们使用了pthread库(原生线程库)创建了线程,那么其他人也可以使用这个pthread库创建线程(其他人在我们电脑上面创建他们所需要的线程) —— 原生线程库存在多个线程
那么原生线程库就要对线程做管理(不然怎么知道线程对应的id,栈在哪里,大小是多少…)
管理 —— 先描述,再组织 —— 描述就是:线程的属性,只不过属性比较少(线程id值,栈区的地址等等)
每一个线程都要有对应的属性,pthread要管理好每一个线程
我们通过线程的id也就是线程在库里面的起始地址,向后就能找到线程在库里面的资源了!
所以我们拿着线程id就能够对线程进行各种操作了
所以,新线程的栈都是独立的,存在共享区当中!
主线程用的是地址空间中的栈,而新线程用的是共享区中pthread原生线程库里面的栈!
我们上面看到了,每一个线程在库中都有一个线程局部存储
#include
#include
#include
#include
#include
#include
using std::cout;
using std::endl;
int g_val = 100;
std::string ThreadId(const pthread_t &pthtred_id)
{
char buffer[128];
snprintf(buffer, sizeof(buffer), "0x%x", pthread_self());
return buffer;
}
void *start_toutine(void *args)
{
std::string name = static_cast<const char *>(args);
while (1)
{
printf(" new thread : g_val : %d &g_val : %p\n", g_val, &g_val);
sleep(1);
g_val++;
}
return nullptr;
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, start_toutine, (void *)"thread 1");
std::string main_tid = ThreadId(pthread_self());
pthread_detach(tid);
while (1)
{
printf("main thread : g_val : %d &g_val : %p\n", g_val, &g_val);
sleep(1);
}
return 0;
}
添加__thred,可以将一个内置类型(int,double…)设置为线程局部存储!
我们接下来就对linux中的轻量级进程进行封装!从而达到C++中直接使用线程的效果!
我们这里实现的就简单一点,能达到C++中直接创建线程,线程调用函数,然后join等待线程就行了
Thread.hpp:
#pragma once
#include
#include
#include
#include
#include
#include
class Thread;
class Context // 上下文,将pthread_create中类内的第4个参数和this合并
{
public:
Context()
: _this(nullptr),
_args(nullptr)
{
}
~Context()
{
}
Thread *_this; // 调用函数的this指针(pthread_create创建线程之后,线程执行函数的参数的this)
void *_args; // 保存线程执行函数的参数
};
class Thread
{
public:
// using func_t = std::function;作用同下
typedef std::function<void *(void *)> func_t;
const int num = 1024;
//这样改就可以像C++一样,直接构造一个线程,然后传线程执行函数就行,不需要传线程函数参数和线程编号了!
//Thread(func_t func, void *args = nullptr, int number = 0)
Thread(func_t func, void *args, int number)
: _func(func),
_args(args)
{
// _name = "thread : ";
// _name += std::to_string(number);//和下面snprintf作用相同
char buffer[num];
snprintf(buffer, sizeof buffer, "thread : %d", number);
_name = buffer;
//void start()//可以直接把start函数拿进来
//{
Context *cnt = new Context();
cnt->_args = _args;
cnt->_this = this;
// 这里直接把cnt进行传参,直接把start_routine的参数和this指针都传过去了
int n = pthread_create(&_tid, nullptr, start_routine, cnt); // 这里_func会报错,C接口不能直接掉C++的函数对象
assert(0 == n); // 线程创建成功函数返回值为0
(void)n; // 编译器在release版本会注释掉assert。会发现我们没有使用n,有的编译器就会报存在未使用变量n的警告,我们这里取消这个警告
//}
}
// 在类内创建线程,想让线程执行对应的方法,需要将方法设置为static(静态方法) —— 因为static类内函数没有this指针!
static void *start_routine(void *args) // 写一个函数,方便我们下面pthread_create第3个参数使用
{
// 很不幸,下面还是不能直接使用start_routine函数,因为start_routine是类内函数,有缺少参数!
// 也就是说start_routine有两个参数,第一个参数是Thread* this指针,第二个参数才是args
// return _func(args);
// 这里就又出问题了,静态函数只能调用静态方法和静态成员,不能调用类内成员方法和成员变量!
// 所以得换一种写法
Context *cnt = static_cast<Context *>(args);
void *ret = cnt->_this->run(cnt->_args); // 这里调用下面的run函数
delete cnt;
return ret;
}
// void start()//这里把start放外面,调用的时候要让线程调用start
// {
// Context *cnt = new Context();
// cnt->_args = _args;
// cnt->_this = this;
// // 这里直接把cnt进行传参,直接把start_routine的参数和this指针都传过去了
// int n = pthread_create(&_tid, nullptr, start_routine, cnt); // 这里_func会报错,C接口不能直接掉C++的函数对象
// assert(0 == n); // 线程创建成功函数返回值为0
// (void)n; // 编译器在release版本会注释掉assert。会发现我们没有使用n,有的编译器就会报存在未使用变量n的警告,我们这里取消这个警告
// }
void join()
{
int n = pthread_join(_tid, nullptr);
assert(n == 0);
(void)n;
printf("%s\n", strerror(n));
}
void *run(void *args) // 给上面start_routine来用的
{
return _func(args);
}
~Thread()
{
}
private:
std::string _name; // 我们想直接看线程名字,比如线程1,线程2这种
pthread_t _tid;
func_t _func; // 线程未来执行的函数
void *_args; // 线程执行函数的参数
};
mythread.cpp:
#include
#include
#include
#include
#include
#include
#include //智能指针
#include "Thread.hpp"
using std::cout;
using std::endl;
void *thread_run(void *args) // 线程执行函数
{
std::string work_type = static_cast<const char *>(args);
while (1)
{
printf("我是一个新线程,我正在做 : %s\n", work_type.c_str());
sleep(1);
}
}
int main()
{
std::unique_ptr<Thread> thread1(new Thread(thread_run, (void *)"thread 1", 1));
std::unique_ptr<Thread> thread2(new Thread(thread_run, (void *)"thread 2", 2));
std::unique_ptr<Thread> thread3(new Thread(thread_run, (void *)"thread 3", 3));
// thread1->start();//如果把Thread.hpp的start放到构造函数外面,这里就要调start函数
// thread2->start();
// thread3->start();
thread1->join();
thread2->join();
thread3->join();
return 0;
}
临界资源:多线程执行流共享的资源就叫做临界资源
临界区:每个线程内部,访问临界资源的代码,就叫做临界区
互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
原子性(后面讨论如何实现):不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成
我们前面知道了全局变量是被所有线程所共享的,那么我们多个线程对这一个全局变量进行–操作,会发生什么呢?
这里我们要看到的现象是 : —— 票有可能被抢为负数!
我们需要让多个线程进行并行交叉执行,usleep休眠切换进程!
多个线程并行交叉执行的本质:cpu内的调度器频繁的发生线程进行切换和调度
线程一般什么时候发生切换呢? —— 1、时间片到了;2、来了更高优先级的线程;3、线程等待的时候
线程什么时候检测:时间片到了,来了优先级更高的线程,线程等待这些问题呢?
线程从内核态返回用户态的时候,线程要对调度状态进行检测,如果可以就直接发生线程切换!
#include
#include
#include
#include
#include
#include
#include
#include "Thread.hpp"
using std::cout;
using std::endl;
// 这里我们要看到的现象是 : —— 票有可能被抢为负数!
// 我们需要让多个线程进行并行交叉执行,usleep休眠切换进程!
// 多个线程并行交叉执行的本质:cpu内的调度器频繁的发生线程进行切换和调度
// 线程一般什么时候发生切换呢? —— 1、时间片到了;2、来了更高优先级的线程;3、线程等待的时候
// 线程什么时候检测:时间片到了,来了优先级更高的线程,线程等待这些问题呢?
// 线程从内核态返回用户态的时候,线程要对调度状态进行检测,如果可以就直接发生线程切换!
int train_tickets = 1000; // 火车票
void *get_train_tickets(void *args)
{
std::string user_name = static_cast<const char *>(args);
while (1)
{
if (train_tickets > 0) // 有票才能抢
{
// 用这段时间来模拟抢票真实需要花费的时间
usleep(1000); // 1s = 1000毫秒 = 1000 000微秒 = 1000 000 000纳秒
printf("%s正在进行抢票 : %d\n", user_name.c_str(), train_tickets);
train_tickets--;
}
else
break;
}
}
int main()
{
std::unique_ptr<Thread> thread1(new Thread(get_train_tickets, (void *)"user 1", 1));
std::unique_ptr<Thread> thread2(new Thread(get_train_tickets, (void *)"user 2", 2));
std::unique_ptr<Thread> thread3(new Thread(get_train_tickets, (void *)"user 3", 3));
thread1->join();
thread2->join();
thread3->join();
return 0;
}
为什么会出现抢到负数这种情况呢?
对一个全局变量进行多线程修改是安全的吗?
答案是:会的! 哪怕只有一个主线程,一个从线程都是不安全的!(除非进程内部只有一个执行流!)
所以,我们上面抢票的代码就是没有if判断语句,只有train_tickets–这个操作,抢票也会抢到负数!只不过因为cpu太高效了,我们不好模拟出来罢了!
我们定义的全局变量,在没有保护的情况下,往往是不安全的!
像上面多个线程并行执行造成的数据安全问题 —— 我们称之为数据不一致问题!
那么,接下来我们就要提出解决数据不一致问题的方案了!
要解决以上问题,需要做到三点:
1、代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
2、如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
3、如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区
要做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫互斥量
1、多个执行流进行安全访问的共享资源,我们称之为 —— 临界资源(上面的共享资源tickets不是安全的,所以不是临界资源)
2、我们把多个执行流中,访问临界资源的代码,称之为 —— 临界区! —— 往往是线程代码很小的一部分!
3、想让多个线程串形访问共享资源 ——互斥
!(上面3个线程抢票,一个一个的按顺序来,不能并行抢票)
4、对一个资源进行操作的时候,要么不做,要么做完 —— 原子性
对资源进行操作,如果只有一条汇编指令,那么该操作就是原子的(这种情况是原子性的一小部分)
pthread_mutex_init()函数 功能:初始化一个互斥锁
pthread_mutex_destroy()函数 功能:销毁一个互斥锁
pthread_mutex_lock()函数 功能:加锁
pthread_mutex_trylock()函数 功能:尝试加锁
pthread_mutex_unlock()函数 功能:解锁
以上5个函数的返回值都是:成功返回0, 失败返回错误号。
pthread_mutex_t 类型,其本质是一个结构体。为简化理解,应用时可忽略其实现细节,简单当成整数看待。如:
pthread_mutex_t mutex; 变量mutex只有两种取值1、0。
我们定义锁可以定义为全局的,也可以定义为局部的
但是:如果锁是局部的:我们要调用pthread_mutex_init()和pthread_mutex_destroy()对锁进行初始化和销毁
如果锁的全局的:pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;//全局锁直接使用PTHREAD_MUTEX_INITIALIZER初始化就行
我们见见猪跑
#include
#include
#include
#include
#include
#include
#include
#include "Thread.hpp"
using std::cout;
using std::endl;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;//全局锁直接使用PTHREAD_MUTEX_INITIALIZER初始化就行
int train_tickets = 1000;
void *get_train_tickets(void *args)
{
std::string user_name = static_cast<const char *>(args);
while (1)
{
pthread_mutex_lock(&mutex);
if (train_tickets > 0)
{
usleep(1000);
printf("%s正在进行抢票 : %d\n", user_name.c_str(), train_tickets);
train_tickets--;
pthread_mutex_unlock(&mutex);
}
else
{
pthread_mutex_unlock(&mutex);//如果if为假,直接走else,那么锁没有被解锁,所以这里要解锁
break;
}
}
}
int main()
{
std::unique_ptr<Thread> thread1(new Thread(get_train_tickets, (void *)"user 1", 1));
std::unique_ptr<Thread> thread2(new Thread(get_train_tickets, (void *)"user 2", 2));
std::unique_ptr<Thread> thread3(new Thread(get_train_tickets, (void *)"user 3", 3));
thread1->join();
thread2->join();
thread3->join();
return 0;
}
上面抢票确实是安全的,但是都是由一个线程抢完了,也就是说哪一个线程先被加锁了,它就会一直抢票,直到结束,然后解锁
这个情况我们等下再解释,我们先来看看局部变量的锁怎么使用
#include
#include
#include
#include
#include
#include
#include
#include
#include "Thread.hpp"
using std::cout;
using std::endl;
int train_tickets = 1000;
class ThreadData
{
public:
ThreadData(std::string threadname, pthread_mutex_t *lock)
: _threadname(threadname),
_lock(lock)
{
}
~ThreadData()
{
}
public: // 为了方便下面使用,就不定义为private了
std::string _threadname;
pthread_mutex_t *_lock;
};
void *get_train_tickets(void *args)
{
ThreadData *td = static_cast<ThreadData *>(args);
while (1)
{
pthread_mutex_lock(td->_lock);
if (train_tickets > 0)
{
usleep(1000);
printf("%s正在进行抢票 : %d\n", td->_threadname.c_str(), train_tickets);
train_tickets--;
pthread_mutex_unlock(td->_lock);
}
else
{
pthread_mutex_unlock(td->_lock);
break;
}
//不能在这里解锁,不然上面else的break直接跳出循环了,锁没有被解开
}
}
int main()
{
#define NUM 4
pthread_mutex_t lock;//这把锁是公共的锁
pthread_mutex_init(&lock, nullptr);
std::vector<pthread_t> tids(NUM);
for (size_t i = 0; i < NUM; ++i)
{
char buffer[128];
snprintf(buffer, sizeof buffer, "thread %d", i + 1); // i+1使得新线程id不从0开始
ThreadData *td = new ThreadData(buffer, &lock);
pthread_create(&tids[i], nullptr, get_train_tickets, td);
}
for (const auto &tid : tids)
{
pthread_join(tid, nullptr);
}
pthread_mutex_destroy(&lock);
// pthread_t t1, t2, t3, t4;
// pthread_create(&t1, nullptr, get_train_tickets, (void *)"thread 1");
// pthread_create(&t2, nullptr, get_train_tickets, (void *)"thread 2");
// pthread_create(&t3, nullptr, get_train_tickets, (void *)"thread 3");
// pthread_create(&t4, nullptr, get_train_tickets, (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;
}
加锁和解锁的过程是被多个线程串行执行的,这就导致程序变慢了!
锁只规定互斥访问,没有规定必须让谁优先执行
所以,锁就是多个执行流竞争的结果!(谁竞争到了锁就是谁的)
当然,我们线程抢完票就什么都不做了吗?
当然不是的,我们线程抢到票了,还要给用户汇报抢到票的消息,以及处理其他工作!
线程要使用锁达到安全访问共享资源的目的,那么线程要先看到锁才行!
所以,锁被每一个线程都看到了,锁就是一个共享资源!
锁是用来保护全局资源的(共享资源),锁本身也是一个全局资源(共享资源),那么谁来保护锁呢?
所以,pthread_mutex_lock/pthread_mutex_unlock : 加锁和解锁操作必须是安全的!(加锁操作是原子的!)
如果申请锁成功了,那么继续向后执行代码,那么如果申请没有成功呢?
如果申请锁没有成功,执行流会被阻塞!线程进入休眠状态
这种申请失败,使执行流阻塞的锁我们称为 —— 挂起等待锁
谁持有锁,谁进入临界区
未来,我们写代码的时候,如果要多线程访问公共资源(共享资源),我们要么对线程全部加锁,要么就全部不加锁
不能一部分线程加锁,一部分线程不加锁,这算我们代码是有bug的!
所以,我们申请锁的操作一定要是安全的,原子的,因为锁是一个共享资源,还要起到保护共享资源的作用,如果锁都是一个不安全的操作,怎么保护共享资源呢?所以锁也是要被保护的,而保护锁的操作就是 —— 申请锁是原子操作
加锁的过程是原子的!而我们对于解锁其实没有太多的要求,因为就你一个线程拿到锁了,其他人都没有,正常进行解锁操作,把锁让出来就行
接下来我们来研究一下:互斥量实现原理(也就是互斥锁是怎么样保证加锁是原子操作的)
经过上面的例子,大家已经意识到单纯的 i++ 或者 ++i 都不是原子的,有可能会有数据一致性问题
为了实现互斥锁操作,大多数==体系结构(cpu架构,比如x86_32)==都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。
在硬件层面上面,我们可以通过地址总线使线程/进程不会被切换的操作,让进程/线程达到原子性。比如:时间片到了,优先级更高的进程/线程来了,进程/线程要进行等待等一系列要切换进程/线程的操作,通过地址总线对切换操作进行忽略
当然一般除非遇到一个进程/线程特别特别重要的时候才会作用操作,大部分情况下都不会这么做
1、cpu内部只有一套寄存器,被所有执行流共享
2、cpu内部寄存器存放的内容,是每一个执行流私有的,是每一个执行流运行时的上下文结构
Mutex.hpp:
#pragma once
#include
#include
using std::cout;
using std::endl;
class Mutex
{
public:
Mutex(pthread_mutex_t *lock_p = nullptr)
: _lock_p(lock_p)
{
}
void lock()
{
if (_lock_p)//锁不为空,才表示要设置锁
pthread_mutex_lock(_lock_p);
}
void unlock()
{
if (_lock_p)//锁不为空,表示有锁需要我们解锁
pthread_mutex_unlock(_lock_p);
}
~Mutex() {}
private:
pthread_mutex_t *_lock_p;
};
class LockGuard
{
public:
LockGuard(Mutex mutex)
:_mutex(mutex)
{
_mutex.lock();//在构造函数中加锁
}
~LockGuard()
{
_mutex.unlock();//在析构函数中解锁
}
private:
Mutex _mutex;
};
#include
#include
#include
#include
#include
#include
#include
#include
#include "Thread.hpp"
#include "Mutex.hpp"
using std::cout;
using std::endl;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
int train_tickets = 1000;
class ThreadData
{
public:
ThreadData(std::string threadname, pthread_mutex_t *lock)
: _threadname(threadname),
_lock(lock)
{
}
~ThreadData()
{
}
public:
std::string _threadname;
pthread_mutex_t *_lock;
};
void *get_train_tickets(void *args)
{
std::string user_name = static_cast<const char *>(args);
while (1)
{
{//不想把usleep也加锁,加一个花括号,就相当于一个作用域了
// 加锁
LockGuard lockguard(&lock);//RAII操作
if (train_tickets > 0)
{
usleep(1000);
printf("%s正在进行抢票 : %d\n", user_name.c_str(), train_tickets);
train_tickets--;
}
else
{
break;//这里break也不怕了,因为出了作用域,lockguard对象自动销毁,调用析构
}
}
usleep(1000);
}
}
int main()
{
pthread_t t1, t2, t3, t4;
pthread_create(&t1, nullptr, get_train_tickets, (void *)"thread 1");
pthread_create(&t2, nullptr, get_train_tickets, (void *)"thread 2");
pthread_create(&t3, nullptr, get_train_tickets, (void *)"thread 3");
pthread_create(&t4, nullptr, get_train_tickets, (void *)"thread 4");
pthread_join(t1, nullptr);
pthread_join(t2, nullptr);
pthread_join(t3, nullptr);
pthread_join(t4, nullptr);
return 0;
}
线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。
重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数
不保护共享变量的函数
函数状态随着被调用,状态发生变化的函数
返回指向静态变量指针的函数
调用线程不安全函数的函数
每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的
类或者接口对于线程来说都是原子操作
多个线程之间的切换不会导致该接口的执行结果存在二义性
多线程访问共享资源,全局变量不加保护会引发线程安全问题,但是多线程访问局部资源就不会
调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的
调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构
可重入函数体内使用了静态的数据结构
不使用全局变量或静态变量
不使用用malloc或者new开辟出的空间
不调用不可重入函数
不返回静态或全局数据,所有数据都有函数的调用者提供
使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据
函数是可重入的,那就是线程安全的
函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。
可重入函数是线程安全函数的一种
线程安全不一定是可重入的,而可重入函数则一定是线程安全的
如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的
可重入函数是线程安全的一种,一个函数是可重入函数,那么这个函数就算线程安全的
但是,线程安全的函数,不一定是可重入函数
一个函数能被多个执行流执行,切不会出错,那么这个函数就算可重入函数,出错就是不可重入函数
一个代码片段被多个线程调用,如果没有引发数据不安全问题,如果有,那么这个线程就算不安全的,没有就是线程安全的
死锁是指在一组进程中的各个线程(轻量级进程)均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态
在多把锁的刺激下,我们持有自己的锁不释放,还要对方的锁,对方也是如此,此时就容易造成死锁!
一把锁可能产生死锁吗?是可以的(我们重复申请一把锁两次/多次!)
为什么会有死锁呢?
我们后面会再讲,这里我们先来讲讲逻辑链条
为什么会有死锁呢?因为我们用了锁 <——为什么我们要用锁呢?保证临界资源的安全<——为什么要保证临界资源的安全呢?因为多线程访问我们可能出现数据不一致问题<——为什么会产生数据不一致问题呢?因为我们用了多线程,并且访问了全局资源<——多线程大部分资源(全局资源)是共享的<——多线程的特性!
任何技术都有自己的边界,是解决问题的,但有可能在解决问题的同时,一定会引入新的问题!
互斥条件:一个资源每次只能被一个执行流使用
请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
循环等待条件(环路等待条件):若干执行流之间形成一种头尾相接的循环等待资源的关系
如果我们要得到一个死锁,那么这4个条件必须满足!!!
我们来仔细分析:
1、互斥:我们访问某些资源的时候必须是互斥的!(线程串行访问),没有互斥就代表没有加锁,锁都没有还谈什么死锁
2、请求与保持:我要你的锁(请求),我不释放我的锁(保持)
3、不剥夺:我要你的,但是我不抢你的,要让你自愿释放锁(这叫不剥夺);【我要你的,但是我通过优先级,或者手动设置的状态,允许我过来抢过你的,这就是剥夺!】
4、环路等待条件:A有自己的锁,不释放,还要B的锁;B有自己的锁,不释放,还要C的锁;C有自己的锁,不释放,还要A的锁。这样就形成了一个环路!
那么我们要怎么来破坏死锁呢?
破坏死锁的四个必要条件:
(破坏一个必要条件就行了:
1、破坏互斥 : 互斥是锁的特性,这个肯定是不能被破坏的
2、破坏请求与保持:我们有一个锁了,再申请锁的时候,就会申请失败,这个时候,我们如果申请失败,我们把锁给释放掉
3、破坏不剥夺:我们讲锁设置为可以剥夺的就行,我们通过线程的优先级,或者手动设置的状态,让你被迫放弃你的锁,然后将锁给我4、破坏环路等待条件 : 让线程申请锁的顺序一致(多线程同时先申请A锁,然后B锁,然后C锁) )
加锁顺序一致(破坏环路等待条件)
避免锁未释放的场景(破坏请求与保持)
资源一次性分配(要加锁的地方,直接一次性全部加锁,不要打散式的加锁)
死锁检测算法(了解) —— 锁里面有一个计数器,如果线程检测这个计数器长时间没有发生变化,线程就会自动进行解锁
银行家算法(了解)
一个线程申请锁,可以有另一个线程来释放锁
加锁也是要根据实际情况来使用的,我们上面的抢票中,票全被一个线程抢完了,这有错误吗?没有错误,但是这是不合理的!
当我们安全访问临界资源的条件下,让多个线程按照顺序来进行访问,从而避免饥饿问题,这就叫同步!
条件变量是pthread给我们提供的一种数据类型,我们定义了条件变量之后,阻塞等待的线程在条件变量下面进行等待(阻塞线程一个个链接在条件变量下面,如同单链表一样)
1、当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。
2、例如一个线程访问队列时,发现队列为空,线程一直加锁,判断队列有没有节点,没有然后解锁。一直重复这个工作,这个过程没有错误,但是严重不合理
我们要做到一个线程访问队列时,发现队列为空,它只能等待,直到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量
(抢票也是,我今天发布了1000张票,黄牛通过线程抢完之后,黄牛只能通过线程拿到锁然后检测是否有票,有就抢,没有就什么都做不了【因为获取票的条件没有满足 : 没有票!】)
样例:
面试官在一个酒店的房间里面进行招聘,那么这个面试官就是一个共享资源,并且因为有房间(锁)的保护,一次只能有一个人进行面试。但是房间外面的人可能不讲武德,有一个人就进去面试,感觉面的不好,又进去面试了一遍,面试官还没有认出来,这样循环式的让一个人一直面试,就导致其他人的饥饿问题了,其他人都不能参加面试了
这个时候来了一个比较强的面试官
当条件不满足的时候,多线程必须去某些定义好的条件变量进行等待。等条件满足了,再从条件变量下面唤醒线程!
初始化
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrictattr);
参数:
cond:要初始化的条件变量
attr:NULL
销毁
int pthread_cond_destroy(pthread_cond_t *cond)
等待条件满足
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
参数:
cond:要在这个条件变量上等待
mutex:互斥量,后面详细解释
唤醒等待
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
老规矩,见见猪跑:
#include
#include
#include
#include
using std::cout;
using std::endl;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; // 这里就将条件变量和锁定义为全局的,后面会使用init和destroy函数
pthread_cond_t cond = PTHREAD_COND_INITIALIZER; // 条件变量本身不是互斥的,所以要配和互斥锁来使用
int tickets = 1000;
void *start_routine(void *args)
{
std::string name = static_cast<const char *>(args);
while (1)
{
pthread_mutex_lock(&lock);
// if (tickets > 0)//判断暂时省略
pthread_cond_wait(&cond, &lock); // 这里为什么要有lock锁,我们后面说
cout << name << "->" << tickets << endl;
cout.flush();
tickets--;
pthread_mutex_unlock(&lock);
}
}
int main()
{
// 通过条件变量控制线程的执行
pthread_t t1, t2;
pthread_create(&t1, nullptr, start_routine, (void *)"thread 1");
pthread_create(&t2, nullptr, start_routine, (void *)"thread 2");
while (1)
{
sleep(1);
pthread_cond_signal(&cond);
cout << "main thread wakeup one thread..." << endl;
cout.flush();
}
pthread_join(t1, nullptr);
pthread_join(t2, nullptr);
return 0;
}
生产消费模型:是一种多执行流协同的方式
为什么要协同呢?多个执行流各自执行各自的不行吗?
我们多线程在访问共享资源的时候,如果不对共享资源进行加锁保护,会引发数据不一致问题。加锁之后我们确实保证了数据安全的问题,但是像我们上面抢票那样,一个线程直接把票抢完了?导致其他线程饥饿问题
这有错吗?没有错,但是这合理吗?不合理!
所以,我们在多线程工作的场景下面,既要保证数据的安全,又要保证多线程在工作的时候按照特定的顺序来访问。这种顺序可以不是一种绝对顺序,但是一定要有顺序,尽可能保证每个线程都能合适合理的访问某种资源。所以我们需要一种多线程在这种场景下面工作的模式,这种模式最经典,最常见的就叫做 —— 生产消费模型!
拿火腿肠举例。我们消费者一般不能直接去找供货商买火腿肠,因为我们吃火腿肠一次顶多吃几串,就算一个人去买两箱,但是又许多人都去供货商哪里买,供货商卖给我们火腿肠的成本还不如开机器,养员工的成本!所以,我们去找供货商买货品,供货商卖给我们是亏本生意,一般不买!其次就是,供货商的工厂一般在远离市中心的地方,我们去供货商路途较远,我们买货物所消耗的钱可能还没有路费花的多。所以,就算我们跑到供货商门口供货商也不会卖给我们,因为这是亏本生意
那么供货商将一批大量的货物生产好之后,怎么将货物销售给我们消费者呢?这个时候就要通过中间人
来向我们消费者提供货物了,这个中间人就是 —— 超市。超市:集中需求,分发产品 —— 超市将我们各个消费者的需求集中起来,然后提供供货商提供的货物,将这些货物销售给消费者
(供货商一次性将一批产品出售给各地区的超市,然后再由超人作为中间人出售给我们消费者,这样供货商既不会亏本,消费者也可以轻松进行消费)
所以,现实生活中,学生等购买货物的人是消费者,供货商等提供货物的人是生产者,而超市这个中间人就是生产者和消费者的交易场所!
而有了这个超市交易场所之后,供货商的人有可能在进行加工生成商品,也可能过年在家休息;而我们学生消费者,可能在玩游戏,刷抖音。
有了超市之后,我们就【把这种生产者和消费者互不干扰的行为,用计算机的语言就称为 生产者过程和消费者过程的 —— 解耦!】
而当我们消费者一直消费的时候,把超市的东西买完了,生产者一直不向超市提供货物,这就没有货物供我们购买了;
当生产者一直生产向超市提供货物,而超市都放不下生产者的货物了,但是没有一个消费者来购买,这就导致超市没有空间来存放货物了
所以,【超市只不过是临时保存货物的场所,用计算机语言就称为 缓冲区!】
有了缓冲区之后,在缓冲区有足够空间的前提下,通过这个缓冲区,生产者可以一直向缓冲区提供数据;而消费者也可以一直向缓冲区消费数据。这就使得 : 生产者和消费者的步调并不怎么一致,从而达到生产者过程和消费者过程的 —— 解耦
而上面的缓冲区起到了生产者过程和消费者过程的解耦,那么我们举一个没有解耦样例
我们在main函数中调用fun函数,给fun函数传参
其中:
调用fun函数的一方:生成了数据
形成的变量 : 变量暂时保存数据
目标函数 : 消费了数据
而当我们main函数调用fun函数的时候,main函数此时什么都不能做,要等fun函数调用完返回,main函数才能继续向下执行
所以,main函数和fun函数就是一种强耦合关系(我们上面买火腿肠的例子中,一个消费者去供货商买一根火腿肠,这个时候供货商对这个消费者说,你在这里等着,我去开机器给你做一根火腿肠,这个时候消费者只能等着,这就是生产者和消费者的强耦合)
我们通过上面知道了 —— 生产者生产货物到超市,而消费者要在超市进行消费 —— 所以,生产者和消费者要都能看到超市才行 —— 那么这个超市就是一个共享资源!
那么就会出现这种情况 : 当我们买火腿肠的时候,超市没有了,而生产者正在往超市架子上面放,这个时候我们能不能买火腿肠成功呢?是不确定的!因为生产者向超市架子摆放火腿肠这个操作是原子的,要么摆上去了,要么没有摆上去,这个时候我们能不能买到火腿肠取决于生产者有没有把火腿肠放到超市架子上面
所以,如果不对共享资源进行保护(超市),那么就会引发数据不一致问题!所以共享资源(超市)要被保护起来!
我们的生产者在计算机中,是一个或者多个线程
我们的消费者在计算机中,也是一个或者多个线程
共享资源(超市)要怎么被被保护起来呢?
我们就先要讨论一下,生产者与生产者,消费者与消费者,生产者与消费者之间的关系了:
1、生产者和生产者之间的关系 : 互斥关系(两家火腿肠供货商,超市要么摆你的火腿肠,要么摆我的火腿肠。可以先摆你的火腿肠,卖完之后再摆我的,但是不能一起摆,不然混在一起消费者不好购买!所以,对于超市固定的框架来说我们处于竞争关系,用计算机语言来说我们处于互斥,有我没你,有你没我)
2、消费者与消费者之间的关系 : 互斥关系(两个消费者看上了同一份数据,互不相让,通过竞争之后才能知道哪一个消费者拿到了这份数据。硬件设计语言来说两个消费者处于互斥,要么我拿到了这份数据,要么你拿到了这份数据)
3、生产者和消费者之间的关系 : 同步与互斥关系
(互斥关系 :我们生产者生产了一份hello world,消费者要读出这份hello world。消费者刚刚读出hello,生产者就直接将剩下的world改成了bit,这就导致了消费者读取出来的数据是hello bit。这就引发了数据不一致问题了!所以生产者和消费者是互斥关系,只允许一方对数据进行访问!
同步关系:当超市没有货物的时候,一个消费者来问有没有货物,超市工作人员说没有,但是今天或者这段时间这个消费者一直来问工作人员有没有货物,工作人员一直回答没有。而我们不止一个消费者,肯定有很多消费者,如果每一个消费者都这么一直问,那么,既浪费了每一个消费者的时间,又浪费了工作人员的时间,本来超市工作人员可以用这些时间来催促供货商提供货物的
同理!当我们超市货物摆满了的时候,没有人来消费,而生产者一直询问超市工作人员超市有没有空间,工作人员也回答说没有,这个时候多个生产者一直询问,而工作人员也一直回答没有,这就又浪费生产者的时间,又浪费了工作人员的时间,本来工作人员可以用这些时间来通知消费者过来消费的!
所以,我们可以让超市工作人员加上消费者和生产者的微信,有来货物通知消费者来消费,货物不足的时候通知生产者来生产货物 —— 通过这种策略保证消费者过程和生产者过程协同起来。维护生产者与消费者之间的同步关系,从而提高整个生产消费模型的效率!)
我们生产消费模型,遵循一个原则:
"321"原则:
3种关系 :生产者和生产者之间的关系(互斥);消费者和消费者之间的关系(互斥);生产者和消费者之间的关系(同步【保证共享资源的安全性】与互斥) —— 产品(本质是数据)
2种角色:生产者线程和消费者线程
1个交易场所:一段特定结构的缓冲区
我们想写生产消费模型,本质工作就是维护"321"原则
1、生产线程和消费线程进行解耦(缓冲区可以缓存数据,消费线程不用一直等生产线程,通过缓冲区可以各做各的)
2、支持生产和消费在一段时间内忙闲不均的问题(消费者消费的很快,生产者生产的很慢,不用担心,缓冲区缓存了一批数据,消费者直接来消费就行,等消费者消费慢下来了,再找生产者加载数据到缓冲区里面;消费者消费的很慢,生产者生产的很快,也不用担心,缓冲区会将生产者生产的数据都保存起来,等生产者生产慢下来的时候再通知消费者来进行消费
这样就使得生产者生产的快慢和消费者消费的快慢没有互相影响!)
3、提高效率(生产者专门生产,消费者专门消费,因为有缓冲区的存在,不需要消费者直接找生产者,等生产者生产,生产者也不需要等消费者到了,然后进行生产)
但是还是有问题 : 生产消费模型的特点有一个提高效率,但是如果我缓冲区已经满了,但是生产者还向缓冲区打数据,这个时候就不行,得等消费者来消费;而缓冲区没有数据了,消费者也不能进行消费,要得生产者向缓冲区打数据!这不还是要等吗?这么就提高效率了呢?
再者就是,我们如果只维护生产消费模型互斥关系是肯定不行的,因为我们对共享资源(缓冲区)要进行保护,加锁和解锁,如果生产者拿到锁之后,进行判断,缓冲区有没有满,没有满就向缓冲区输入数据,然后解锁。当缓冲区满了的时候,生产者线程拿到锁,然后判断,缓冲区满了,直接解锁。然后生产者线程又拿到锁,判断缓冲区满了,然后解锁…像这样生产者线程拿到锁之后判断然后解锁,什么都不做一直占用锁资源!
所以,我们生产消费模型不能只维护互斥关系,还要维护同步关系!
我们上面7-4学习了pthread_cond_wait和pthread_cond_signal函数,这里我们来以这上面的知识为基点,继续深入学习
BlockingQueue
在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)
这个BlockingQueue就是我们上面说的一段特定结构的缓冲区(超市)
这个BlockQueue就是共享资源!它可以为空,可以为满(有最大上限限制),这就约束了我们的生产者和消费者在特定情况下应该阻塞住
我们这里直接用C++中stl容器的queue来充当BlockingQueue
先来看看最基础的版本:
BlockingQueue.hpp
#pragma once
#include
#include
#include
static const int gmaxcap = 5;
template <class T>
class BlockingQueue
{
public:
BlockingQueue(const int &maxcap = gmaxcap)
: _maxcap(maxcap)
{
pthread_mutex_init(&_mutex, nullptr);
pthread_cond_init(&_pcond, nullptr);
pthread_cond_init(&_ccond, nullptr);
}
void push(const T &in) // 输入型参数 —— const &
{
pthread_mutex_lock(&_mutex);
// 细节2: 充当条件判断的语法必须是while,不能用if
while(is_full())
//if (is_full())
// 细节1:pthread_cond_wait这个函数的第二个参数,必须是我们正在使用的互斥锁!不然我们线程在条件变量下等待的时候抱着锁直接跑了,其他线程都不能申请成功锁了
// a. pthread_cond_wait: 该函数调用的时候,会以原子性的方式,将锁释放,并将自己挂起
// b. pthread_cond_wait: 该函数在被唤醒返回,继续向下执行代码的时候,会自动的重新获取你传入的锁
pthread_cond_wait(&_pcond, &_mutex); // 生产条件不满足,无法生产,生产者进行等待
// 走到这里阻塞队列里面一定没有满
_q.push(in);
// 阻塞队列里面一定有数据
pthread_cond_signal(&_ccond); // 细节3:pthread_cond_signal:这个函数,可以放在临界区内部,也可以放在外部
pthread_mutex_unlock(&_mutex);
//pthread_cond_signal(&_pcond);也可以放在这里
}
void pop(T *out) // 输出型参数 —— * //输入输出型参数 —— &
{
pthread_mutex_lock(&_mutex);
while(is_empty())
//if (is_empty())
pthread_cond_wait(&_ccond, &_mutex);
// 走到这里阻塞队列里面一定不为空
*out = _q.front();
_q.pop();
// 阻塞队列里面一定有一个空位置
pthread_cond_signal(&_pcond);// 细节3:pthread_cond_signal:这个函数,可以放在临界区内部,也可以放在外部
pthread_mutex_unlock(&_mutex);
//pthread_cond_signal(&_pcond);也可以放在这里
}
~BlockingQueue()
{
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_pcond);
pthread_cond_destroy(&_ccond);
}
private:
bool is_empty()
{
return _q.empty();
}
bool is_full()
{
return _q.size() == _maxcap;
}
private:
std::queue<T> _q;
int _maxcap; // 队列最大上限
pthread_mutex_t _mutex;
pthread_cond_t _pcond; // 生产者的条件变量
pthread_cond_t _ccond; // 消费者的条件变量
};
Main.cc:
#include
#include
#include
#include
#include
#include "BlockingQueue.hpp"
using std::cout;
using std::endl;
void *consumer(void *args)
{
BlockingQueue<int> *bq = static_cast<BlockingQueue<int> *>(args);
while (1)
{
// 消费
int data;
bq->pop(&data);
cout << "消费数据 : " << data << endl;
// sleep(1);
}
return nullptr;
}
void *producter(void *args)
{
BlockingQueue<int> *bq = static_cast<BlockingQueue<int> *>(args);
while (1)
{
// 生产
int data = rand() % 10 + 1;
bq->push(data);
cout << "生产数据 : " << data << endl;
sleep(1);//生产速度慢下来,消费数据也必须慢,因为这是阻塞队列
}
return nullptr;
}
int main()
{
srand((unsigned long)time(nullptr) ^ getpid());
BlockingQueue<int> *bq = new BlockingQueue<int>();
pthread_t c, p;
pthread_create(&c, nullptr, consumer, bq);
pthread_create(&p, nullptr, producter, bq);
pthread_join(c, nullptr);
pthread_join(p, nullptr);
delete bq;
return 0;
}
如果不用signal函数唤醒,而是使用broadcast函数唤醒一批线程,如果使用if判断!那么我们的_q.push和_p.pop操作可能执行了多次!!!当我们阻塞队列如果为空,或者为满的时候,只有一个线程执行后面的代码,其他线程都在while里面循环,再次执行wait函数,到对应的条件变量上面等待
当然了,我们都用模板了,就为了搞个int类型这么简单吗?
当然不是,我们是可以向阻塞队列里面放任务的!
我们来继续改进代码:
BlockQueue.hpp上面都没有没有改,但还是放在这里好统一观看:
#pragma once
#include
#include
#include
static const int gmaxcap = 5;
template <class T>
class BlockingQueue//生产和消费必须都能够先看到这个阻塞队列
{
public:
BlockingQueue(const int &maxcap = gmaxcap)
: _maxcap(maxcap)
{
pthread_mutex_init(&_mutex, nullptr);
pthread_cond_init(&_pcond, nullptr);
pthread_cond_init(&_ccond, nullptr);
}
void push(const T &in) // 输入型参数 —— const &
{
pthread_mutex_lock(&_mutex);
// if (is_full())
while (is_full())
{
pthread_cond_wait(&_pcond, &_mutex); // 生产条件不满足,无法生产,生产者进行等待
}
// 走到这里阻塞队列里面一定没有满
_q.push(in);
// 阻塞队列里面一定有数据
pthread_cond_signal(&_ccond); // 阻塞队列有数据了,我们唤醒消费者,开始进行消费
pthread_mutex_unlock(&_mutex);
}
void pop(T *out) // 输出型参数 —— * //输入输出型参数 —— &
{
pthread_mutex_lock(&_mutex);
// if (is_empty())
while (is_empty())
{
pthread_cond_wait(&_ccond, &_mutex);
}
// 走到这里阻塞队列里面一定不为空
*out = _q.front();
_q.pop();
// 阻塞队列里面一定有一个空位置
pthread_cond_signal(&_pcond);
pthread_mutex_unlock(&_mutex);
}
~BlockingQueue()
{
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_pcond);
pthread_cond_destroy(&_ccond);
}
private:
bool is_empty()
{
return _q.empty();
}
bool is_full()
{
return _q.size() == _maxcap;
}
private:
std::queue<T> _q;
int _maxcap; // 队列最大上限
pthread_mutex_t _mutex;
pthread_cond_t _pcond; // 生产者的条件变量
pthread_cond_t _ccond; // 消费者的条件变量
};
Main.cc:
#include
#include
#include
#include
#include
#include
#include "BlockingQueue.hpp"
#include "Task.hpp"
using std::cerr;
using std::cout;
using std::endl;
const std::string oper = "+-*/%";
int my_math(int x, int y, char op)
{
int result = 0;
switch (op)
{
case '+':
// return x + y;//return 不需要break
result = x + y;
break;
case '-':
// return x - y;
result = x - y;
break;
case '*':
// return x * y;
result = x * y;
break;
case '/':
{
if (y == 0)
{
cerr << "div zero error!" << endl;
result = -1;
}
else
result = x / y;
}
break;
case '%':
{
if (y == 0)
{
cerr << "mod zero error!" << endl;
result = -1;
}
else
result = x % y;
// return x % y;
}
break;
default:
break;
}
return result;
}
int myadd(int x, int y)
{
return x + y;
}
void *consumer(void *args)
{
BlockingQueue<Task> *bq = static_cast<BlockingQueue<Task> *>(args);
while (1)
{
// 消费
Task t;
bq->pop(&t);
cout << "消费任务 : " << t() << endl; // 仿函数 ->()
// cout << "消费任务 : ";
// cout << t() << endl;
sleep(1);
// int data;
// bq->pop(&data);
// cout << "消费数据 : " << data << endl;
// sleep(1);
}
return nullptr;
}
void *producter(void *args)
{
BlockingQueue<Task> *bq = static_cast<BlockingQueue<Task> *>(args);
while (1)
{
// 生产
int x = rand() % 10 + 1;
// int y = rand() % 5 + 1;
int y = rand() % 5; // 不加1可能会%到0
char op = rand() % oper.size();
Task t(x, y, oper[op], my_math);
bq->push(t);
cout << "生产任务 : " << t.TaskToString() << endl;
sleep(1);
// cout << "生产数据 : " << t << endl;
sleep(1);//生产速度慢下来,消费数据也必须慢,因为这是阻塞队列
}
return nullptr;
}
int main()
{
srand((unsigned long)time(nullptr) ^ getpid());
BlockingQueue<Task> *bq = new BlockingQueue<Task>();
pthread_t c, p;
pthread_create(&c, nullptr, consumer, bq);
pthread_create(&p, nullptr, producter, bq);
pthread_join(c, nullptr);
pthread_join(p, nullptr);
delete bq;
return 0;
}
Task.hpp:
#pragma once
#include
#include
#include
#include
#include
class Task
{
using func_t = std::function<int(int, int, char)>; // 这里也要加上一个char,不然回调函数和Main文件里面的my_math函数参数对不上
// typedef function func_t;
public:
Task()
{
}
Task(int x, int y, char op, func_t func)
: _x(x),
_y(y),
_op(op),
_callback(func)
{
}
// int operator()() // 仿函数
std::string operator()()
{
int result = _callback(_x, _y, _op);
char buffer[1024];
snprintf(buffer, sizeof(buffer), "%d %c %d = %d", _x, _op, _y, result);
return buffer;
// return result;
}
std::string TaskToString()
{
char buffer[1024];
snprintf(buffer, sizeof(buffer), "%d %c %d = ?", _x, _op, _y);
return buffer;
}
~Task()
{
}
private:
int _x;
int _y;
char _op;
func_t _callback;
};
这里出现乱序的原因是,我们的生产和消费的打印没有加锁,while是死循环打印,难免出现乱序问题
我们接下来继续改进,我们有了一个生产,一个消费
我们还可以这样改:
1号线程生产派发任务,通过阻塞队列1,2号线程消费处理任务,然后2号线程再通过阻塞队列2,3号线程将2号线程的处理结果保存在文件中
#pragma once
#include
#include
#include
static const int gmaxcap = 500;
template <class T>
class BlockingQueue//生产和消费必须都能够先看到这个阻塞队列
{
public:
BlockingQueue(const int &maxcap = gmaxcap)
: _maxcap(maxcap)
{
pthread_mutex_init(&_mutex, nullptr);
pthread_cond_init(&_pcond, nullptr);
pthread_cond_init(&_ccond, nullptr);
}
void push(const T &in) // 输入型参数 —— const &
{
pthread_mutex_lock(&_mutex);
// if (is_full())
while (is_full())
{
pthread_cond_wait(&_pcond, &_mutex); // 生产条件不满足,无法生产,生产者进行等待
}
// 走到这里阻塞队列里面一定没有满
_q.push(in);
// 阻塞队列里面一定有数据
pthread_cond_signal(&_ccond); // 阻塞队列有数据了,我们唤醒消费者,开始进行消费
pthread_mutex_unlock(&_mutex);
}
void pop(T *out) // 输出型参数 —— * //输入输出型参数 —— &
{
pthread_mutex_lock(&_mutex);
// if (is_empty())
while (is_empty())
{
pthread_cond_wait(&_ccond, &_mutex);
}
// 走到这里阻塞队列里面一定不为空
*out = _q.front();
_q.pop();
// 阻塞队列里面一定有一个空位置
pthread_cond_signal(&_pcond);
pthread_mutex_unlock(&_mutex);
}
~BlockingQueue()
{
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_pcond);
pthread_cond_destroy(&_ccond);
}
private:
bool is_empty()
{
return _q.empty();
}
bool is_full()
{
return _q.size() == _maxcap;
}
private:
std::queue<T> _q;
int _maxcap; // 队列最大上限
pthread_mutex_t _mutex;
pthread_cond_t _pcond; // 生产者的条件变量
pthread_cond_t _ccond; // 消费者的条件变量
};
Task.hpp:
#pragma once
#include
#include
#include
#include
#include
#include
using std::cerr;
using std::cout;
using std::endl;
const std::string oper = "+-*/%";
int my_math(int x, int y, char op)
{
int result = 0;
switch (op)
{
case '+':
// return x + y;//return 不需要break
result = x + y;
break;
case '-':
// return x - y;
result = x - y;
break;
case '*':
// return x * y;
result = x * y;
break;
case '/':
{
if (y == 0)
{
cerr << "div zero error!" << endl;
result = -1;
}
else
result = x / y;
}
break;
case '%':
{
if (y == 0)
{
cerr << "mod zero error!" << endl;
result = -1;
}
else
result = x % y;
// return x % y;
}
break;
default:
break;
}
return result;
}
class CalTask
{
using func_t = std::function<int(int, int, char)>; // 这里也要加上一个char,不然回调函数和Main文件里面的my_math函数参数对不上
// typedef function func_t;
public:
CalTask()
{
}
CalTask(int x, int y, char op, func_t func)
: _x(x),
_y(y),
_op(op),
_callback(func)
{
}
// int operator()() // 仿函数
std::string operator()()
{
int result = _callback(_x, _y, _op);
char buffer[1024];
snprintf(buffer, sizeof(buffer), "%d %c %d = %d", _x, _op, _y, result);
return buffer;
// return result;
}
std::string TaskToString()
{
char buffer[1024];
snprintf(buffer, sizeof(buffer), "%d %c %d = ?", _x, _op, _y);
return buffer;
}
~CalTask()
{
}
private:
int _x;
int _y;
char _op;
func_t _callback;
};
void savethread(const std::string &massage)
{
const std::string target = "./log.tex";
FILE *fp = fopen(target.c_str(), "a+");
if (fp == nullptr)
{
cerr << "fopen file error!" << endl;
return;
}
fputs(massage.c_str(), fp);
fprintf(fp, "\n");
fclose(fp);
}
class SaveTask
{
typedef std::function<void(const std::string &)> func_t;
public:
SaveTask()
{
}
SaveTask(const std::string &massage, func_t func)
: _massage(massage),
_func(func)
{
}
~SaveTask()
{
}
void operator()()
{
_func(_massage);
}
private:
std::string _massage;
func_t _func;
};
Main.cc:
#include
#include
#include
#include
#include
#include
#include "BlockingQueue.hpp"
#include "Task.hpp"
using std::cerr;
using std::cout;
using std::endl;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
template <class C, class S> // C:计算,S:存储
class BlockQueues
{
public:
BlockingQueue<C> *c_bp;
BlockingQueue<S> *s_bp;
};
void *producter(void *args)
{
// BlockQueues *bq = static_cast *>(args);
BlockingQueue<CalTask> *bq = (static_cast<BlockQueues<CalTask, SaveTask> *>(args))->c_bp;
while (1)
{
// 生产
int x = rand() % 10 + 1;
// int y = rand() % 5 + 1;
int y = rand() % 5; // 不加1可能会%到0
char op = rand() % oper.size();
CalTask t(x, y, oper[op], my_math);
bq->push(t);
pthread_mutex_lock(&mutex);
cout << "producter thread 生产计算任务 : " << t.TaskToString() << endl;
pthread_mutex_unlock(&mutex);
sleep(1);
// cout << "生产数据 : " << t << endl;
sleep(1);//生产速度慢下来,消费数据也必须慢,因为这是阻塞队列
}
return nullptr;
}
void *consumer(void *args)
{
// BlockQueues *bq = static_cast *>(args);
BlockingQueue<CalTask> *bp = (static_cast<BlockQueues<CalTask, SaveTask> *>(args))->c_bp; // 2号线程从这个队列拿数据,进行消费处理
BlockingQueue<SaveTask> *save_bp = (static_cast<BlockQueues<CalTask, SaveTask> *>(args))->s_bp; // 2号线程向这个队列放数据,给3号线程进行消费
while (1)
{
// 消费 —— 计算任务
CalTask t;
// bq->c_bp->pop(&t);//对应BlockQueues *bq = static_cast *>(args);
bp->pop(&t);
std::string result = t(); // 将消费处理结果,赋值给result
pthread_mutex_lock(&mutex);
cout << "consumer thread 消费计算任务 : " << result << endl; // 仿函数 ->()
pthread_mutex_unlock(&mutex);
// 生产 —— 存储任务
SaveTask save(result, savethread);
save_bp->push(save);
pthread_mutex_lock(&mutex);
cout << "推送保存任务完成..." << endl; // 仿函数 ->()
pthread_mutex_unlock(&mutex);
// sleep(1);
}
return nullptr;
}
void *saver(void *args)
{
BlockingQueue<SaveTask> *save_bp = (static_cast<BlockQueues<CalTask, SaveTask> *>(args))->s_bp;
while (1)
{
SaveTask t;
save_bp->pop(&t);
t();
cout << "saver thread 保存任务完成..." << endl; // 仿函数 ->()
}
return nullptr;
}
int main()
{
srand((unsigned long)time(nullptr) ^ getpid());
BlockQueues<CalTask, SaveTask> bps;
bps.c_bp = new BlockingQueue<CalTask>();
bps.s_bp = new BlockingQueue<SaveTask>();
// BlockingQueue *task_bq = new BlockingQueue();//两个阻塞队列
// BlockingQueue *save_bq = new BlockingQueue();
pthread_t c, p, s;
pthread_create(&p, nullptr, producter, &bps);
pthread_create(&c, nullptr, consumer, &bps);
pthread_create(&s, nullptr, saver, &bps);
pthread_join(c, nullptr);
pthread_join(p, nullptr);
pthread_join(s, nullptr);
delete bps.c_bp;
delete bps.s_bp;
return 0;
}
我们可以直接改成多线程模式,这里就不贴代码了,因为push和pop里面有锁,所以多个生产者和多个消费者也是竞争关系,有锁了就不会引发数据不一致等错误
所以就提出来了两个问题:
1、创建多线程生产和消费的意义?
在多线程环境下面,避免不了多个执行流访问同一个共享资源的情况,但是,如果一个执行流长时间持有锁,或者它抢占锁的能力更强,就会导致其他线程出现饥饿问题 —— 而生产消费模型是最常见解决这种问题的方法!
2、生产消费模型高效在哪里?
对于生产者 : 生产者的任务从哪里来呢? -> 从数据库,从网络,从外设等等地方拿来的用户数据 -> 所以,生产者获取任务和构建任务是要花时间的!
对于消费者 : 消费者拿到任务之后,后续有很大概率处理任务非常非常耗时!
所以,生产消费模型高效,并不是体现在阻塞队列拿任务/数据高效。而是!让多个线程可以同时的去执行/计算多个任务/数据,并且还不会影响到其他的线程继续从任务队列里面拿任务;同理,生产者拿数据的时候,获取任务和构建任务要花费时间,而这个时间内其他的线程也可以拿数据到队列里面,并且不会影响到其他的生产者线程
生产消费模型高效在哪里? : 可以在生产之前,消费之后,让线程并行执行!
本节内容比较多,所以分了几篇文章来进行讲解!后面也都是重要的内容!所以要一步步把基础的知识学好,理解清楚,不然后面学起来很吃力