文章目录
前言
如何看待之前学习的单进程?具有一个线程执行流的进程
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、Linux线程概念
1-1、什么是线程?
1-1-1、如何看待页表
1、地址空间是进程能看到的资源窗口(内核区,栈区,堆区…)
2、页表(页表+MMU内存管理单元)决定进程真正拥有资源的情况(每一个进程都认为自己独有4GB空间,可真正拥有多少物理资源由页表映射关系决定)
3、合理对 地址空间+页表 进行资源划分,我们就可以对一个进程的所有资源进行分类(进程看到的资源通过地址空间将虚拟内存划分为:内核区,栈区,堆区…通过页表映射到不同的物理内存)
1-1-2、回顾进程地址空间
系统调用接口开始就会有汇编指令帮助我们陷入内核,这个时候我们的U/K权限就自动切换为K权限,然后就可以通过物理地址找到os,开始执行系统调用了
str先通过虚拟地址找到了物理地址,但是后续的RWX权限str只有R权限,而*str = 'H'
是W权限,所以硬件会报错,os识别到这个硬件报错就是11号信号段错误,然后向进程发送信号,进程收到信号之后,默认动作就是终止进程
1-1-3、页表怎么进行虚拟地址到物理地址的映射的?
就算os把页目录映射的页表的全部都加载进来,其实也不大,因为我们虚拟地址最后的12个比特位是没有进行映射的,它是页内偏移量
1-1-4、Linux中线程的概念(重点)
线程是进程内的一个执行流! 可能很多人看到这里就很懵了,这是很多书上的说法,那么为什么书上要这么说呢?因为操作系统太宏观了,有许多的版本(不同的平台底层实现多线程的方法不同!)。而线程是进程内部都一个执行流这句话放到所有的操作系统上面都是对的!
所以,我们具体就只具体来谈谈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
1-1-5、原生线程库
只认线程,程序员也只认线程(都不认你linux所谓的轻量级进程)
linux无法直接提供创建线程的系统调用接口!而只能提供创建轻量级进程的接口!
所以,为了方便用户创建线程,程序员编写一个用户级线程库(原生线程库:pthread库),它位于我们用户层和系统调用接口之间,我们使用线程库提供的接口,该库就会将我们对线程的操作内部转换为对轻量级进程的操作
pthread线程库 :在任何linux操作系统下面都有 —— 原生线程库
这里提个问题:
pthread库是这么和linux中的轻量级进程建立连接的呢?(也就是怎么把我们对线程的操作,在内部转换为对轻量级进程的操作?)
1-1-6、代码测试
返回值为0表示创建线程成功!
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <cassert>
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是多少
我们将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-1-7、知识点(重点)
1、线程一旦被创建,几乎所有的资源都是被所有线程所共享的!
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <cassert>
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中,被进程多次访问,被进程较高概率命中的数据(是需要跑一段时间才能知道的)
所以,cache里面的热点数据的加载,才是线程为什么切换比进程切换做的工作要少的原因!
4、计算密集型应用和I/O密集型应用
计算密集型应用:主要是进程/线程使用的资源是 :cpu的资源(加密,解密,算法等等 —— 打包压缩,执行打包和压缩对应的算法)
I/O密集型应用:主要是进程/线程使用的资源是 :外设的资源(访问磁盘,访问网络,显示器等等 —— 抖音,迅雷)
5、多线程健壮性问题
简单来说:多个线程共享大部分数据,而一个线程在使用某个数据的时候,另一个线程可能正在对该数据进行修改 —— 一个线程影响到了另外一个线程(缺乏访问控制)
我们先来看看正常情况的代码:
#include <iostream>
#include <string>
#include <pthread.h>
#include <unistd.h>
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-1-8、小结
1-1-8-1、什么是线程
1、在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”
2、一切进程至少都有一个执行线程
3、线程在进程内部运行,本质是在进程地址空间内运行
4、在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化
5、透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流
1-1-8-2、线程的优点
1、创建一个新线程的代价要比创建一个新进程小得多(对比于进程)
2、与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多(对比于进程)
3、线程占用的资源要比进程少很多(对比于进程)
4、能充分利用多处理器的可并行数量(—— 从这里开始下面的优点进程也有)
5、在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
6、计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
7、I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。
1-1-8-3、线程的缺点
1、性能损失
一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
2、健壮性降低
编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。(一个线程异常了可能会影响到其他线程)
3、缺乏访问控制
进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
4、编程难度提高
编写与调试一个多线程程序比单线程程序困难得多
1-1-8-4、线程异常
1、单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃
2、线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出
1-1-8-5、线程用途
1、合理的使用多线程,能提高CPU密集型程序的执行效率
2、合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)
2、Linux进程VS线程
进程是资源分配的基本单位
线程是调度的基本单位
线程共享进程数据,但也拥有自己的一部分数据:
1、 线程ID
2、一组寄存器
3、栈
4、errno
5、信号屏蔽字
6、调度优先级
进程的多个线程共享 同一地址空间,因此Text Segment(代码段)、Data Segment(数据段)都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:
1、文件描述符表
2、每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
3、当前工作目录
4、用户id和组id
进程线程关系图(4种模型):
2-1、轻量级进程接口
我们前面说了,linux只提供了创建轻量级进程的接口,那么这个轻量级进程接口是什么呢?
fork和vfork等函数的底层调用的就是这个clone函数!(ptrhead_create -> clone)
我们不用clone这个函数,而是对应的原生线程库在用这个函数
3、Linux线程控制
3-1、POSIX线程库
1、与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头的
2、要使用这些函数库,要通过引入头文<pthread.h>
3、链接这些线程函数库时要使用编译器命令的“-lpthread”选项
3-2、创建线程
功能:创建一个新的线程
原型
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;失败返回错误码
3-3、错误检查
传统的一些函数是,成功返回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会讲
3-4、创建一批线程
一次性创建10个线程:
#include <iostream>
#include <string>
#include <vector>
#include <pthread.h>
#include <unistd.h>
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;
}
在这10个线程的基础上面,让每一个线程都打印自己的编号!
#include <iostream>
#include <string>
#include <vector>
#include <pthread.h>
#include <unistd.h>
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 <iostream>
#include <string>
#include <vector>
#include <pthread.h>
#include <unistd.h>
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个线程调用!这个函数现在就是重入状态!是一个可重入函数
在函数内定义的变量,都叫做局部变量,具有临时性 —— 在多线程的情况下也没有问题 —— 每一个线程都有独立的栈结构!
3-5、线程终止
3-5-1、return返回
线程函数调用完毕,return的时候就终止了!
3-5-2、pthread_exit函数
需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了
任何一个执行流调用exit都会让整个进程退出
pthread_exit函数
只终止线程,不终止进程!
3-5-3、pthread_cancel(线程取消)
线程是可以被cancel取消的!
前提:线程要运行起来了,才能被取消!
一个线程被取消,它的退出码就是-1
3-6、线程等待
线程也是要被等待的!—— 如果不等待,会造成类似于僵尸进程的问题 —— 内存泄漏!
为什么要线程等待:
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 <iostream>
#include <string>
#include <vector>
#include <pthread.h>
#include <unistd.h>
#include <cstdlib>
#include <cassert>
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等待的 —— 阻塞式等待
3-7、线程执行函数的返回值
一句话 : 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默认函数会调用成功。不考虑异常问题,异常问题是进程考虑的!
3-8、语言层的pthread库
#include <iostream>
#include <unistd.h>
#include <thread>
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线程库意义:给语言级别的线程提供底层的接口支持
3-9、线程分离
我们上面学到了主线程调用pthread_join函数进行阻塞式等待从线程,而线程是没有非阻塞等待的!
默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏
如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源
int pthread_detach(pthread_t thread);
可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离:
pthread_detach(pthread_self());
joinable和分离是冲突的,一个线程不能既是joinable又是分离的
我们来看看正常情况下的代码:
#include <iostream>
#include <pthread.h>
#include <string>
#include <unistd.h>
#include <cstring>
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 <iostream>
#include <pthread.h>
#include <string>
#include <unistd.h>
#include <cstring>
#include <cstdio>
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;
}
接下来我们就来进行线程分离
3-9-1、pthread_detach
先来看看pthread_jion等待值的情况:
#include <iostream>
#include <pthread.h>
#include <string>
#include <unistd.h>
#include <cstring>
#include <cstdio>
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 <iostream>
#include <pthread.h>
#include <string>
#include <unistd.h>
#include <cstring>
#include <cstdio>
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;
}
3-10、pthread库的深入研究
我们使用了pthread库(原生线程库)创建了线程,那么其他人也可以使用这个pthread库创建线程(其他人在我们电脑上面创建他们所需要的线程) —— 原生线程库存在多个线程
那么原生线程库就要对线程做管理(不然怎么知道线程对应的id,栈在哪里,大小是多少…)
管理 —— 先描述,再组织 —— 描述就是:线程的属性,只不过属性比较少(线程id值,栈区的地址等等)
每一个线程都要有对应的属性,pthread要管理好每一个线程
3-10-1、用户级线程id
我们通过线程的id也就是线程在库里面的起始地址,向后就能找到线程在库里面的资源了!
所以我们拿着线程id就能够对线程进行各种操作了
所以,新线程的栈都是独立的,存在共享区当中!
主线程用的是地址空间中的栈,而新线程用的是共享区中pthread原生线程库里面的栈!
3-10-2、线程的局部存储
我们上面看到了,每一个线程在库中都有一个线程局部存储
#include <iostream>
#include <pthread.h>
#include <string>
#include <unistd.h>
#include <cstring>
#include <cstdio>
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…)设置为线程局部存储!
4、线程封装(重点)
我们接下来就对linux中的轻量级进程进行封装!从而达到C++中直接使用线程的效果!
我们这里实现的就简单一点,能达到C++中直接创建线程,线程调用函数,然后join等待线程就行了
Thread.hpp:
#pragma once
#include <iostream>
#include <pthread.h>
#include <string>
#include <cstring>
#include <functional>
#include <cassert>
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<void*(void*)>;作用同下
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 <iostream>
#include <pthread.h>
#include <string>
#include <unistd.h>
#include <cstring>
#include <cstdio>
#include <memory> //智能指针
#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;
}
5、Linux线程互斥
5-1、进程线程间的互斥相关背景概念
临界资源:多线程执行流共享的资源就叫做临界资源
临界区:每个线程内部,访问临界资源的代码,就叫做临界区
互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
原子性(后面讨论如何实现):不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成
5-2、提出问题
我们前面知道了全局变量是被所有线程所共享的,那么我们多个线程对这一个全局变量进行–操作,会发生什么呢?
这里我们要看到的现象是 : —— 票有可能被抢为负数!
我们需要让多个线程进行并行交叉执行,usleep休眠切换进程!
多个线程并行交叉执行的本质:cpu内的调度器频繁的发生线程进行切换和调度
线程一般什么时候发生切换呢? —— 1、时间片到了;2、来了更高优先级的线程;3、线程等待的时候
线程什么时候检测:时间片到了,来了优先级更高的线程,线程等待这些问题呢?
线程从内核态返回用户态的时候,线程要对调度状态进行检测,如果可以就直接发生线程切换!
#include <iostream>
#include <pthread.h>
#include <string>
#include <unistd.h>
#include <cstring>
#include <cstdio>
#include <memory>
#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上提供的这把锁叫互斥量
5-3、解决问题 : 锁
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 <iostream>
#include <pthread.h>
#include <string>
#include <unistd.h>
#include <cstring>
#include <cstdio>
#include <memory>
#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 <iostream>
#include <pthread.h>
#include <string>
#include <vector>
#include <unistd.h>
#include <cstring>
#include <cstdio>
#include <memory>
#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;
}
加锁和解锁的过程是被多个线程串行执行的,这就导致程序变慢了!
锁只规定互斥访问,没有规定必须让谁优先执行
所以,锁就是多个执行流竞争的结果!(谁竞争到了锁就是谁的)
当然,我们线程抢完票就什么都不做了吗?
当然不是的,我们线程抢到票了,还要给用户汇报抢到票的消息,以及处理其他工作!
5-3-1、如何理解锁
线程要使用锁达到安全访问共享资源的目的,那么线程要先看到锁才行!
所以,锁被每一个线程都看到了,锁就是一个共享资源!
锁是用来保护全局资源的(共享资源),锁本身也是一个全局资源(共享资源),那么谁来保护锁呢?
所以,pthread_mutex_lock/pthread_mutex_unlock : 加锁和解锁操作必须是安全的!(加锁操作是原子的!)
如果申请锁成功了,那么继续向后执行代码,那么如果申请没有成功呢?
如果申请锁没有成功,执行流会被阻塞!线程进入休眠状态
这种申请失败,使执行流阻塞的锁我们称为 —— 挂起等待锁
谁持有锁,谁进入临界区
未来,我们写代码的时候,如果要多线程访问公共资源(共享资源),我们要么对线程全部加锁,要么就全部不加锁
不能一部分线程加锁,一部分线程不加锁,这算我们代码是有bug的!
所以,我们申请锁的操作一定要是安全的,原子的,因为锁是一个共享资源,还要起到保护共享资源的作用,如果锁都是一个不安全的操作,怎么保护共享资源呢?所以锁也是要被保护的,而保护锁的操作就是 —— 申请锁是原子操作
5-3-2、如何理解加锁和解锁的本质
加锁的过程是原子的!而我们对于解锁其实没有太多的要求,因为就你一个线程拿到锁了,其他人都没有,正常进行解锁操作,把锁让出来就行
接下来我们来研究一下:互斥量实现原理(也就是互斥锁是怎么样保证加锁是原子操作的)
经过上面的例子,大家已经意识到单纯的 i++ 或者 ++i 都不是原子的,有可能会有数据一致性问题
为了实现互斥锁操作,大多数==体系结构(cpu架构,比如x86_32)==都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。
在硬件层面上面,我们可以通过地址总线使线程/进程不会被切换的操作,让进程/线程达到原子性。比如:时间片到了,优先级更高的进程/线程来了,进程/线程要进行等待等一系列要切换进程/线程的操作,通过地址总线对切换操作进行忽略
当然一般除非遇到一个进程/线程特别特别重要的时候才会作用操作,大部分情况下都不会这么做
现在我们把lock和unlock的伪代码改一下:
1、cpu内部只有一套寄存器,被所有执行流共享
2、cpu内部寄存器存放的内容,是每一个执行流私有的,是每一个执行流运行时的上下文结构
5-3-3、对锁进行封装设计
Mutex.hpp:
#pragma once
#include <iostream>
#include <pthread.h>
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 <iostream>
#include <pthread.h>
#include <string>
#include <vector>
#include <unistd.h>
#include <cstring>
#include <cstdio>
#include <memory>
#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;
}
5-4、可重入VS线程安全
线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。
重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数
5-4-1、常见的线程不安全的情况
不保护共享变量的函数
函数状态随着被调用,状态发生变化的函数
返回指向静态变量指针的函数
调用线程不安全函数的函数
5-4-2、常见的线程安全的情况
每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的
类或者接口对于线程来说都是原子操作
多个线程之间的切换不会导致该接口的执行结果存在二义性
多线程访问共享资源,全局变量不加保护会引发线程安全问题,但是多线程访问局部资源就不会
5-4-3、常见不可重入的情况
调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的
调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构
可重入函数体内使用了静态的数据结构
5-4-4、常见可重入的情况
不使用全局变量或静态变量
不使用用malloc或者new开辟出的空间
不调用不可重入函数
不返回静态或全局数据,所有数据都有函数的调用者提供
使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据
5-4-5、可重入与线程安全联系
函数是可重入的,那就是线程安全的
函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。
5-4-6、可重入与线程安全区别
可重入函数是线程安全函数的一种
线程安全不一定是可重入的,而可重入函数则一定是线程安全的
如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的
可重入函数是线程安全的一种,一个函数是可重入函数,那么这个函数就算线程安全的
但是,线程安全的函数,不一定是可重入函数
5-4-7、小结
一个函数能被多个执行流执行,切不会出错,那么这个函数就算可重入函数,出错就是不可重入函数
一个代码片段被多个线程调用,看看有没有引发数据不安全问题,如果有,那么这个线程就算不安全的,没有就是线程安全的
6、常见锁概念
6-1、死锁
死锁是指在一组进程中的各个线程(轻量级进程)均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态
在多把锁的刺激下,我们持有自己的锁不释放,还要对方的锁,对方也是如此,此时就容易造成死锁!
一把锁可能产生死锁吗?是可以的(我们重复申请一把锁两次/多次!)
为什么会有死锁呢?
我们后面会再讲,这里我们先来讲讲逻辑链条
为什么会有死锁呢?因为我们用了锁 <——为什么我们要用锁呢?保证临界资源的安全<——为什么要保证临界资源的安全呢?因为多线程访问我们可能出现数据不一致问题<——为什么会产生数据不一致问题呢?因为我们用了多线程,并且访问了全局资源<——多线程大部分资源(全局资源)是共享的<——多线程的特性!
任何技术都有自己的边界,是解决问题的,但有可能在解决问题的同时,一定会引入新的问题!
6-2、死锁的4个必要条件
互斥条件:一个资源每次只能被一个执行流使用
请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
循环等待条件(环路等待条件):若干执行流之间形成一种头尾相接的循环等待资源的关系
如果我们要得到一个死锁,那么这4个条件必须满足!!!
我们来仔细分析:
1、互斥:我们访问某些资源的时候必须是互斥的!(线程串行访问),没有互斥就代表没有加锁,锁都没有还谈什么死锁
2、请求与保持:我要你的锁(请求),我不释放我的锁(保持)
3、不剥夺:我要你的,但是我不抢你的,要让你自愿释放锁(这叫不剥夺);【我要你的,但是我通过优先级,或者手动设置的状态,允许我过来抢过你的,这就是剥夺!】
4、环路等待条件:A有自己的锁,不释放,还要B的锁;B有自己的锁,不释放,还要C的锁;C有自己的锁,不释放,还要A的锁。这样就形成了一个环路!
6-3、避免死锁
那么我们要怎么来破坏死锁呢?
破坏死锁的四个必要条件:
(破坏一个必要条件就行了:
1、破坏互斥 : 互斥是锁的特性,这个肯定是不能被破坏的
2、破坏请求与保持:我们有一个锁了,再申请锁的时候,就会申请失败,这个时候,我们如果申请失败,我们把锁给释放掉
3、破坏不剥夺:我们讲锁设置为可以剥夺的就行,我们通过线程的优先级,或者手动设置的状态,让你被迫放弃你的锁,然后将锁给我4、破坏环路等待条件 : 让线程申请锁的顺序一致(多线程同时先申请A锁,然后B锁,然后C锁) )
加锁顺序一致(破坏环路等待条件)
避免锁未释放的场景(破坏请求与保持)
资源一次性分配(要加锁的地方,直接一次性全部加锁,不要打散式的加锁)
6-4、避免死锁算法
死锁检测算法(了解) —— 锁里面有一个计数器,如果线程检测这个计数器长时间没有发生变化,线程就会自动进行解锁
银行家算法(了解)
一个线程申请锁,可以有另一个线程来释放锁
加锁也是要根据实际情况来使用的,我们上面的抢票中,票全被一个线程抢完了,这有错误吗?没有错误,但是这是不合理的!
7、Linux线程同步
7-1、同步的概念
当我们安全访问临界资源的条件下,让多个线程按照顺序来进行访问,从而避免饥饿问题,这就叫同步!
7-2、条件变量
条件变量是pthread给我们提供的一种数据类型,我们定义了条件变量之后,阻塞等待的线程在条件变量下面进行等待(阻塞线程一个个链接在条件变量下面,如同单链表一样)
1、当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。
2、例如一个线程访问队列时,发现队列为空,线程一直加锁,判断队列有没有节点,没有然后解锁。一直重复这个工作,这个过程没有错误,但是严重不合理
我们要做到一个线程访问队列时,发现队列为空,它只能等待,直到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量
(抢票也是,我今天发布了1000张票,黄牛通过线程抢完之后,黄牛只能通过线程拿到锁然后检测是否有票,有就抢,没有就什么都做不了【因为获取票的条件没有满足 : 没有票!】)
将阻塞挂起的线程,放到条件变量下面进行等待,然后被唤醒再拿走
7-3、理解条件变量
样例:
面试官在一个酒店的房间里面进行招聘,那么这个面试官就是一个共享资源,并且因为有房间(锁)的保护,一次只能有一个人进行面试。但是房间外面的人可能不讲武德,有一个人就进去面试,感觉面的不好,又进去面试了一遍,面试官还没有认出来,这样循环式的让一个人一直面试,就导致其他人的饥饿问题了,其他人都不能参加面试了
这个时候来了一个比较强的面试官
当条件不满足的时候,多线程必须去某些定义好的条件变量进行等待。等条件满足了,再从条件变量下面唤醒线程!
画一张图:
7-4、条件变量函数
初始化
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 <iostream>
#include <pthread.h>
#include <string>
#include <unistd.h>
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;
}
8、生产者消费者模型
8-1、举例说明
生产消费模型:是一种多执行流协同的方式
为什么要协同呢?多个执行流各自执行各自的不行吗?
我们多线程在访问共享资源的时候,如果不对共享资源进行加锁保护,会引发数据不一致问题。加锁之后我们确实保证了数据安全的问题,但是像我们上面抢票那样,一个线程直接把票抢完了?导致其他线程饥饿问题
这有错吗?没有错,但是这合理吗?不合理!
所以,我们在多线程工作的场景下面,既要保证数据的安全,又要保证多线程在工作的时候按照特定的顺序来访问。这种顺序可以不是一种绝对顺序,但是一定要有顺序,尽可能保证每个线程都能合适合理的访问某种资源。所以我们需要一种多线程在这种场景下面工作的模式,这种模式最经典,最常见的就叫做 —— 生产消费模型!
拿火腿肠举例。我们消费者一般不能直接去找供货商买火腿肠,因为我们吃火腿肠一次顶多吃几串,就算一个人去买两箱,但是又许多人都去供货商哪里买,供货商卖给我们火腿肠的成本还不如开机器,养员工的成本!所以,我们去找供货商买货品,供货商卖给我们是亏本生意,一般不买!其次就是,供货商的工厂一般在远离市中心的地方,我们去供货商路途较远,我们买货物所消耗的钱可能还没有路费花的多。所以,就算我们跑到供货商门口供货商也不会卖给我们,因为这是亏本生意
那么供货商将一批大量的货物生产好之后,怎么将货物销售给我们消费者呢?这个时候就要通过中间人
来向我们消费者提供货物了,这个中间人就是 —— 超市。超市:集中需求,分发产品 —— 超市将我们各个消费者的需求集中起来,然后提供供货商提供的货物,将这些货物销售给消费者
(供货商一次性将一批产品出售给各地区的超市,然后再由超人作为中间人出售给我们消费者,这样供货商既不会亏本,消费者也可以轻松进行消费)
所以,现实生活中,学生等购买货物的人是消费者,供货商等提供货物的人是生产者,而超市这个中间人就是生产者和消费者的交易场所!
而有了这个超市交易场所之后,供货商的人有可能在进行加工生成商品,也可能过年在家休息;而我们学生消费者,可能在玩游戏,刷抖音。
有了超市之后,我们就【把这种生产者和消费者互不干扰的行为,用计算机的语言就称为 生产者过程和消费者过程的 —— 解耦!】
而当我们消费者一直消费的时候,把超市的东西买完了,生产者一直不向超市提供货物,这就没有货物供我们购买了;
当生产者一直生产向超市提供货物,而超市都放不下生产者的货物了,但是没有一个消费者来购买,这就导致超市没有空间来存放货物了
所以,【超市只不过是临时保存货物的场所,用计算机语言就称为 缓冲区!】
有了缓冲区之后,在缓冲区有足够空间的前提下,通过这个缓冲区,生产者可以一直向缓冲区提供数据;而消费者也可以一直向缓冲区消费数据。这就使得 : 生产者和消费者的步调并不怎么一致,从而达到生产者过程和消费者过程的 —— 解耦
而上面的缓冲区起到了生产者过程和消费者过程的解耦,那么我们举一个没有解耦样例
我们在main函数中调用fun函数,给fun函数传参
其中:
调用fun函数的一方:生成了数据
形成的变量 : 变量暂时保存数据
目标函数 : 消费了数据
而当我们main函数调用fun函数的时候,main函数此时什么都不能做,要等fun函数调用完返回,main函数才能继续向下执行
所以,main函数和fun函数就是一种强耦合关系(我们上面买火腿肠的例子中,一个消费者去供货商买一根火腿肠,这个时候供货商对这个消费者说,你在这里等着,我去开机器给你做一根火腿肠,这个时候消费者只能等着,这就是生产者和消费者的强耦合)
8-2、深入研究
我们通过上面知道了 —— 生产者生产货物到超市,而消费者要在超市进行消费 —— 所以,生产者和消费者要都能看到超市才行 —— 那么这个超市就是一个共享资源!
那么就会出现这种情况 : 当我们买火腿肠的时候,超市没有了,而生产者正在往超市架子上面放,这个时候我们能不能买火腿肠成功呢?是不确定的!因为生产者向超市架子摆放火腿肠这个操作是原子的,要么摆上去了,要么没有摆上去,这个时候我们能不能买到火腿肠取决于生产者有没有把火腿肠放到超市架子上面
所以,如果不对共享资源进行保护(超市),那么就会引发数据不一致问题!所以共享资源(超市)要被保护起来!
我们的生产者在计算机中,是一个或者多个线程
我们的消费者在计算机中,也是一个或者多个线程
共享资源(超市)要怎么被被保护起来呢?
我们就先要讨论一下,生产者与生产者,消费者与消费者,生产者与消费者之间的关系了:
1、生产者和生产者之间的关系 : 互斥关系(两家火腿肠供货商,超市要么摆你的火腿肠,要么摆我的火腿肠。可以先摆你的火腿肠,卖完之后再摆我的,但是不能一起摆,不然混在一起消费者不好购买!所以,对于超市固定的框架来说我们处于竞争关系,用计算机语言来说我们处于互斥,有我没你,有你没我)
2、消费者与消费者之间的关系 : 互斥关系(两个消费者看上了同一份数据,互不相让,通过竞争之后才能知道哪一个消费者拿到了这份数据。硬件设计语言来说两个消费者处于互斥,要么我拿到了这份数据,要么你拿到了这份数据)
3、生产者和消费者之间的关系 : 同步与互斥关系
(互斥关系 :我们生产者生产了一份hello world,消费者要读出这份hello world。消费者刚刚读出hello,生产者就直接将剩下的world改成了bit,这就导致了消费者读取出来的数据是hello bit。这就引发了数据不一致问题了!所以生产者和消费者是互斥关系,只允许一方对数据进行访问!
同步关系:当超市没有货物的时候,一个消费者来问有没有货物,超市工作人员说没有,但是今天或者这段时间这个消费者一直来问工作人员有没有货物,工作人员一直回答没有。而我们不止一个消费者,肯定有很多消费者,如果每一个消费者都这么一直问,那么,既浪费了每一个消费者的时间,又浪费了工作人员的时间,本来超市工作人员可以用这些时间来催促供货商提供货物的
同理!当我们超市货物摆满了的时候,没有人来消费,而生产者一直询问超市工作人员超市有没有空间,工作人员也回答说没有,这个时候多个生产者一直询问,而工作人员也一直回答没有,这就又浪费生产者的时间,又浪费了工作人员的时间,本来工作人员可以用这些时间来通知消费者过来消费的!
所以,我们可以让超市工作人员加上消费者和生产者的微信,有来货物通知消费者来消费,货物不足的时候通知生产者来生产货物 —— 通过这种策略保证消费者过程和生产者过程协同起来。维护生产者与消费者之间的同步关系,从而提高整个生产消费模型的效率!)
8-3、小结
我们生产消费模型,遵循一个原则:
"321"原则:
3种关系 :生产者和生产者之间的关系(互斥);消费者和消费者之间的关系(互斥);生产者和消费者之间的关系(同步【保证共享资源的安全性】与互斥) —— 产品(本质是数据)
2种角色:生产者线程和消费者线程
1个交易场所:一段特定结构的缓冲区
我们想写生产消费模型,本质工作就是维护"321"原则
8-4、生产消费模型的特点
1、生产线程和消费线程进行解耦(缓冲区可以缓存数据,消费线程不用一直等生产线程,通过缓冲区可以各做各的)
2、支持生产和消费在一段时间内忙闲不均的问题(消费者消费的很快,生产者生产的很慢,不用担心,缓冲区缓存了一批数据,消费者直接来消费就行,等消费者消费慢下来了,再找生产者加载数据到缓冲区里面;消费者消费的很慢,生产者生产的很快,也不用担心,缓冲区会将生产者生产的数据都保存起来,等生产者生产慢下来的时候再通知消费者来进行消费
这样就使得生产者生产的快慢和消费者消费的快慢没有互相影响!)
3、提高效率(生产者专门生产,消费者专门消费,因为有缓冲区的存在,不需要消费者直接找生产者,等生产者生产,生产者也不需要等消费者到了,然后进行生产)
但是还是有问题 : 生产消费模型的特点有一个提高效率,但是如果我缓冲区已经满了,但是生产者还向缓冲区打数据,这个时候就不行,得等消费者来消费;而缓冲区没有数据了,消费者也不能进行消费,要得生产者向缓冲区打数据!这不还是要等吗?这么就提高效率了呢?
再者就是,我们如果只维护生产消费模型互斥关系是肯定不行的,因为我们对共享资源(缓冲区)要进行保护,加锁和解锁,如果生产者拿到锁之后,进行判断,缓冲区有没有满,没有满就向缓冲区输入数据,然后解锁。当缓冲区满了的时候,生产者线程拿到锁,然后判断,缓冲区满了,直接解锁。然后生产者线程又拿到锁,判断缓冲区满了,然后解锁…像这样生产者线程拿到锁之后判断然后解锁,什么都不做一直占用锁资源!
所以,我们生产消费模型不能只维护互斥关系,还要维护同步关系!
8-5、基于BlockingQueue的生产者消费者模型
我们上面7-4学习了pthread_cond_wait和pthread_cond_signal函数,这里我们来以这上面的知识为基点,继续深入学习
BlockingQueue
在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)
这个BlockingQueue就是我们上面说的一段特定结构的缓冲区(超市)
这个BlockQueue就是共享资源!它可以为空,可以为满(有最大上限限制),这就约束了我们的生产者和消费者在特定情况下应该阻塞住
我们这里直接用C++中stl容器的queue来充当BlockingQueue
先来看看最基础的版本:
BlockingQueue.hpp
#pragma once
#include <iostream>
#include <queue>
#include <pthread.h>
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 <iostream>
#include <pthread.h>
#include <ctime>
#include <unistd.h>
#include <sys/types.h>
#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 <iostream>
#include <queue>
#include <pthread.h>
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 <iostream>
#include <pthread.h>
#include <ctime>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#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 <iostream>
#include <queue>
#include <pthread.h>
#include <functional>
#include <cstdio>
class Task
{
using func_t = std::function<int(int, int, char)>; // 这里也要加上一个char,不然回调函数和Main文件里面的my_math函数参数对不上
// typedef function<int(int, int)> 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号线程的处理结果保存在文件中
BlockingQueue.hpp还是没改:
#pragma once
#include <iostream>
#include <queue>
#include <pthread.h>
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 <iostream>
#include <queue>
#include <pthread.h>
#include <functional>
#include <cstdio>
#include <cstring>
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<int(int, int)> 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 <iostream>
#include <pthread.h>
#include <ctime>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#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<CalTask, SaveTask> *bq = static_cast<BlockQueues<CalTask, SaveTask> *>(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<CalTask, SaveTask> *bq = static_cast<BlockQueues<CalTask, SaveTask> *>(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<CalTask, SaveTask> *bq = static_cast<BlockQueues<CalTask, SaveTask> *>(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<CalTask> *task_bq = new BlockingQueue<CalTask>();//两个阻塞队列
// BlockingQueue<SaveTask> *save_bq = new BlockingQueue<SaveTask>();
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;
// srand((unsigned long)time(nullptr) ^ getpid());
// BlockQueues<CalTask, SaveTask> bqs;
// bqs.c_bq = new BlockQueue<CalTask>();
// bqs.s_bq = new BlockQueue<SaveTask>();
// pthread_t c[2], p[3], s;
// pthread_create(p, nullptr, productor, &bqs);
// pthread_create(p+1, nullptr, productor, &bqs);
// pthread_create(p+2, nullptr, productor, &bqs);
// pthread_create(c, nullptr, consumer, &bqs);
// pthread_create(c+1, nullptr, consumer, &bqs);
// pthread_create(&s, nullptr, saver, &bqs);
// 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);
return 0;
}
我们可以直接改成多线程模式,这里就不贴代码了,因为push和pop里面有锁,所以多个生产者和多个消费者也是竞争关系,有锁了就不会引发数据不一致等错误
所以就提出来了两个问题:
1、创建多线程生产和消费的意义?
在多线程环境下面,避免不了多个执行流访问同一个共享资源的情况,但是,如果一个执行流长时间持有锁,或者它抢占锁的能力更强,就会导致其他线程出现饥饿问题 —— 而生产消费模型是最常见解决这种问题的方法!
2、生产消费模型高效在哪里?
对于生产者 : 生产者的任务从哪里来呢? -> 从数据库,从网络,从外设等等地方拿来的用户数据 -> 所以,生产者获取任务和构建任务是要花时间的!
对于消费者 : 消费者拿到任务之后,后续有很大概率处理任务非常非常耗时!
所以,生产消费模型高效,并不是体现在阻塞队列拿任务/数据高效。而是!让多个线程可以同时的去执行/计算多个任务/数据,并且还不会影响到其他的线程继续从任务队列里面拿任务;同理,生产者拿数据的时候,获取任务和构建任务要花费时间,而这个时间内其他的线程也可以拿数据到队列里面,并且不会影响到其他的生产者线程
生产消费模型高效在哪里? : 可以在生产之前,消费之后,让线程并行执行!
9、POSIX信号量
POSIX信号量和SystemV信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源目的。 但POSIX可以用于线程间同步
9-1、找不足
我们前面一篇写了比较多的代码了,那么有没有什么"不足"的地方呢?
1、一个线程在操作临界资源的时候,临界资源必须是满足条件的!(生产者生产的时候,队列要有空间;消费者消费的时候,对列要有数据)
2、公共资源是否满足生产或者消费条件,我们是无法直接得知的! —— 我们不能提前知道公共资源是否满足生产/消费条件 —— 我们在没有访问之前是无法得知的(我们在进入这个队列之前,并不知道这个队列是满还是空,所以不知道生产者能不能向队列生产;消费者能不能从队列拿数据进行消费)
假如我们真能提前知道一个线程访问的公共资源能不能满足生产消费条件,我们就不需要加锁,直接让进行生产消费就行,不满足就在条件变量下面等待,但是我们不知道!
3、我们只能先加锁,再操作,再解锁(先加锁 : 我们要检测条件是否满足,而检测的本质也是在访问临界资源,所以while循环外面加锁!)
小结 : 我们在操作临界资源的时候,临界资源有可能不就绪(临界资源当前的状态不能够让我们访问)。但是,我们无法提前得知;所以,只能先加锁,再检测,然后根据检测结果,决定下一步这么走
那么我们能不能在实际操作的时候就得知资源的情况呢?
这就需要用到信号量了!
9-2、信号量
9-2-1、什么是信号量
我们之前讲过一个电影院的故事!
只要我们对资源进行整体加锁,就默认了我们对这个资源的整体使用(我们上一篇写的代码中,每一个线程都将队列当成一个整体来访问)
但是,实际情况存在 : 一份公共资源,允许多个线程访问不同的区域(电影院故事 : 只有我们抢同一个座位的时候发生竞争关系,一般情况下不会发生竞争)
信号量本身也是用来保护临界资源的!
什么是信号量:(下面两点)
1、信号量的本质是一个
计数器
!衡量临界资源数量多少的计数器!
每一个线程访问公共资源的时候,都要先申请信号量!只有申请成功了,才能够保证该线程在公共资源内有该线程的位置,给我们预留着,让我们访问!(这是怎么实现的呢? : 由程序员编码实现的,程序员编码来保证不同的线程可以并发访问公共资源的不同区域!)
我们有一份临界资源,比如说一个数组,我们可以:
1、对该数组加锁,再检测,然后操作的方法让每一个线程依次访问
也可以:2、每一个线程申请信号量,只要是申请信号量成功的人,程序员编码保证,未来这份公共资源一定有你的位置给该线程访问(电影院最多100人,我不管你们怎么抢票,电影院对多就容纳100人,抢到票的【信号量】,你爱来不来,电影结束之前,都有你的位置,而我们电影院这100个人,经过合理的规划【抢到票的位置】,让他们坐到不同的位置,就不会发生冲突,并且还能够并发访问同一份公共资源)
2、只要有信号量,就在未来一定拥有临界资源的一部分
申请信号量的本质 : 对临界资源中特定的一小块资源的预定机制
1、每一个线程访问公共资源的时候,都要先申请信号量
2、信号量的本质是一个计数器
!衡量临界资源数量多少的计数器!
有没有一种可能,我们在访问真正的临界资源之前,我们就可以提前知道临界资源的使用情况了!
现在我们每一个线程都不是先访问公共资源了,而是先申请信号量 ,申请成功的未来一定有公共资源的一小块空间是能够让线程访问的,申请失败只能等线程将公共资源中的一小块资源释放了该线程才能访问公共资源 —— 我们没有访问公共资源,但是知道了公共资源的使用状况
只要申请信号量成功,就一定有你的资源
只有申请信号量失败,就说明条件不满足,只能等
我们就不需要判断临界资源条件是否就绪了
9-2-2为什么要有信号量
因为在访问临界资源之前,我们可以通过申请信号量来得知,访问的临界资源的状态是否就绪了。我们可以用信号量来评估临界资源的使用情况了!
线程要访问临界资源中的某一个区域 —— 要先申请信号量 —— 我们能申请信号量其他人也可以申请 —— 那么我们所有人就必须先看到信号量 —— 信号量本身就是 公共资源
信号量是一个计数器(衡量临界资源数量多少的计数器) —— 那么一定有递加和递减的操作(–/++)
和锁一样!锁是用来保护临界资源的,而所有人都能申请锁,锁要被全部人看到,所以锁是一个公共资源!那么锁只有保证自身的安全性,才能保护临界资源,所以加锁的操作是原子的(其他线程对于锁就两种状态:有锁和没锁)
而信号量也是用来保护临界资源的,和锁同理,信号量也要被所有人看到,也是一个公共资源!所以信号量也必须是原子的!(申请成功就–,失败就等待,不存在第三种状态!)
9-3、信号量的使用接口
初始化信号量
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value); //pthread库为我们维护的sem_t
参数:
pshared:0表示线程间共享,非零表示进程间共享 //一般用0就行
value:信号量初始值 //我们想让信号量为多少,给值就行
返回值:成功返回0,失败返回-1,错误码被设置
销毁信号量
int sem_destroy(sem_t *sem);
等待信号量
功能:等待信号量,会将信号量的值减1
int sem_wait(sem_t *sem); //P() —— P操作
发布信号量
功能:发布信号量,表示资源使用完毕,可以归还资源了。将信号量值加1(+1就达到归还资源的目的了)。
int sem_post(sem_t *sem);//V() —— V操作
9-4、基于环形队列的生产消费模型(重点)
9-4-1、逻辑知识
我们前面学习了阻塞队列的生产消费模型,这个阻塞队列用不用信号量是无所谓的!因为阻塞队列使用信号量进行并发的时候,我们只能对对头和队尾生产数据和消费数据,而使用信号量并发还不如用互斥锁+检测结果+操作的方法实在(定位阻塞队列的中间数据,再使用信号量进行并发访问太过麻烦了)
而我们这里的环形队列生产消费模型使用信号量就是很适合的!
环形队列采用数组模拟,用模运算来模拟环状特性
每一次i和j的移动,都要进行i%=num和j%=num的判断,如果i和j走到了数组最后的位置,然后%num之后就回到了数组的起始位置,这就相当于i和j一直在走一个圆了!(物理上面是数组,逻辑上面是圆)
当然上面不是我们的重点,我们重点的内容在下面:
pc就是生产消费模型,这里为了方便就简写了(p—— producer —— 生产,c —— consumer —— 消费)
结论:环形队列中,大部分情况下单生产和单消费是可以并发执行的(我生产我的,你消费你的)
只有在队列满和队列空的情况下,生产者和消费者指向同一个位置,才会出现互斥与同步的问题!
当生产线程和消费线程在环形队列指向同一个位置的时候 : 只允许一个线程来访问,这就是互斥;如果环形队列为空,如何如何,如果环形队列为满,又如何如何,这就是同步问题(有顺序行的访问,要么我访问,要么你访问)
定义一个信号量,在代码中就是定义一个计数器!
我们定义一个信号量不可以吗?然后用环形队列的空间大小减去信号量,得到剩余的空间不行吗?
这么做是多此一举的!因为环形队列的空间大小减去信号量不是原子的!无法保证安全性,更何况我都有信号量了,多用几个没什么大不了的
刚开始环形队列有10个空间,生产者线程和消费者线程谁先运行不确定(多线程和多进程谁先运行是不确定的!),但是申请信号量成功的一定是生产者,因为队列是空的,comsumer_sem为0
申请自己关心的资源,释放对方关心的资源!
当生产者一直生产,消费者一直不消费的时候,也没问题,prodocter_sem减到0,生产者的P操作就失败了(生产者不能把消费者套一个圈)!反过来消费者一直消费,生产者不生产也是一样的(消费者不能超过生产者)
未来,生产和消费的位置我们要想清楚:
1、生产和消费的位置本质就是队列的下标!
2、生产和消费的位置是两个下标!互不干扰
3、队列为空或者为满的时候,两个下标相同
9-4-2、代码实现
RingQueue.hpp:
#pragma once
#include <iostream>
#include <pthread.h>
#include <vector>
#include <cassert>
#include <functional>
#include <semaphore.h> //信号量所需的头文件
using std::cerr;
using std::cout;
using std::endl;
static const int gcap = 5; // 刚开始给的环形队列的最大空间
template <class T>
class RingQueue
{
public:
RingQueue(const int &cap = gcap)
: _queue(cap), // 环形队列初始化开辟cap个空间
_maxcap(cap)
{
int n = sem_init(&_spacesem, 0, _maxcap); // 初始化两个信号量
assert(0 == n);
n = sem_init(&_datasem, 0, 0);
assert(0 == n);
_ProducerStep = _ConsumerStep = 0; // 刚开始进入队列,生产者和消费者指向同一个位置
}
void Push(const T &in) // 不需要加锁
{
P(_spacesem); // 申请到了空间信号量 —— 能够进行正常的生产
_queue[_ProducerStep++] = in;
_ProducerStep %= _maxcap; // 数组模拟环形队列
V(_datasem); // 生产者没有归还空间,因为数据占着空间了,但是我们多了数据,所以V数据信号量
}
void Pop(T *out)
{
P(_datasem); // 申请数据信号量 —— 能够进行正常的消费
*out = _queue[_ConsumerStep++];
_ConsumerStep %= _maxcap;
V(_spacesem); // 数据被消费了之后还了吗?没有,但是我们多了消费之后的空间,所以V空间信号量
}
~RingQueue()
{
sem_destroy(&_spacesem);
sem_destroy(&_datasem);
}
private:
void P(sem_t &sem) // pv操作
{
int n = sem_wait(&sem);
// assert(0 == n);
// (void)n;
if (n != 0)
printf("P操作失败!\n");
}
void V(sem_t &sem)
{
int n = sem_post(&sem);
// assert(0 == n);
// (void)n;
if (n != 0)
printf("V操作失败!\n");
}
private:
std::vector<T> _queue; // 环形队列
int _maxcap; // 环形队列的最大容量
sem_t _spacesem; // 生产者看中的资源 —— 空间资源
sem_t _datasem; // 消费者看中的资源 —— 数据资源
int _ProducerStep; // 生产者在队列的位置
int _ConsumerStep; // 消费者在队列的位置
};
main.cpp:
#include "RingQueue.hpp"
#include <ctime> //时间戳需要
#include <cstdlib> //srand需要
#include <sys/types.h> //gerpid需要这两个头文件
#include <unistd.h>
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void *producerRoutine(void *args)
{
RingQueue<int> *ringqueue = static_cast<RingQueue<int> *>(args);
while (true)
{
int data = rand() % 10 + 1;
ringqueue->Push(data);
pthread_mutex_lock(&mutex);
cout << " 生产完成,生产数据 : " << data << endl;
pthread_mutex_unlock(&mutex);
sleep(1); // 生产慢一点
}
}
void *ConsumerRoutine(void *args)
{
RingQueue<int> *ringqueue = static_cast<RingQueue<int> *>(args);
while (true)
{
int data;
ringqueue->Pop(&data);
pthread_mutex_lock(&mutex);
cout << " 消费完成,消费数据 : " << data << endl;
pthread_mutex_unlock(&mutex);
}
}
int main()
{
srand((unsigned int)time(nullptr) ^ getpid() ^ pthread_self());
;
RingQueue<int> *rq = new RingQueue<int>();
pthread_t p, c;
pthread_create(&p, nullptr, producerRoutine, rq);
pthread_create(&c, nullptr, ConsumerRoutine, rq);
pthread_join(p, nullptr);
pthread_join(c, nullptr);
delete rq;
return 0;
}
同样的道理,搞了个环形队列生产消费模型出来,就用整形数据来作为任务有的太lou了,我们拷贝上面的任务文件过来
RingQueue.hpp也是什么都没改:
#pragma once
#include <iostream>
#include <pthread.h>
#include <vector>
#include <cassert>
#include <functional>
#include <semaphore.h> //信号量所需的头文件
using std::cerr;
using std::cout;
using std::endl;
static const int gcap = 5; // 刚开始给的环形队列的最大空间
template <class T>
class RingQueue
{
public:
RingQueue(const int &cap = gcap)
: _queue(cap), // 环形队列初始化开辟cap个空间
_maxcap(cap)
{
int n = sem_init(&_spacesem, 0, _maxcap); // 初始化两个信号量
assert(0 == n);
n = sem_init(&_datasem, 0, 0);
assert(0 == n);
_ProducerStep = _ConsumerStep = 0; // 刚开始进入队列,生产者和消费者指向同一个位置
}
void Push(const T &in) // 不需要加锁
{
P(_spacesem); // 申请到了空间信号量 —— 能够进行正常的生产
_queue[_ProducerStep++] = in;
_ProducerStep %= _maxcap; // 数组模拟环形队列
V(_datasem); // 生产者没有归还空间,因为数据占着空间了,但是我们多了数据,所以V数据信号量
}
void Pop(T *out)
{
P(_datasem); // 申请数据信号量 —— 能够进行正常的消费
*out = _queue[_ConsumerStep++];
_ConsumerStep %= _maxcap;
V(_spacesem); // 数据被消费了之后还了吗?没有,但是我们多了消费之后的空间,所以V空间信号量
}
~RingQueue()
{
sem_destroy(&_spacesem);
sem_destroy(&_datasem);
}
private:
void P(sem_t &sem) // pv操作
{
int n = sem_wait(&sem);
// assert(0 == n);
// (void)n;
if (n != 0)
printf("P操作失败!\n");
}
void V(sem_t &sem)
{
int n = sem_post(&sem);
// assert(0 == n);
// (void)n;
if (n != 0)
printf("V操作失败!\n");
}
private:
std::vector<T> _queue; // 环形队列
int _maxcap; // 环形队列的最大容量
sem_t _spacesem; // 生产者看中的资源 —— 空间资源
sem_t _datasem; // 消费者看中的资源 —— 数据资源
int _ProducerStep; // 生产者在队列的位置
int _ConsumerStep; // 消费者在队列的位置
};
Task.hpp:
#pragma once
#include <iostream>
#include <queue>
#include <pthread.h>
#include <functional>
#include <cstdio>
#include <cstring>
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<int(int, int)> 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, 1024, "%d %c %d = ?", _x, _op, _y);
return buffer;
}
~CalTask()
{
}
private:
int _x;
int _y;
char _op;
func_t _callback;
};
main.cpp:
#include "RingQueue.hpp"
#include "Task.hpp"
#include <ctime> //时间戳需要
#include <cstdlib> //srand需要
#include <sys/types.h> //gerpid需要这两个头文件
#include <unistd.h>
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void *producerRoutine(void *args)
{
RingQueue<CalTask> *ringqueue = static_cast<RingQueue<CalTask> *>(args);
while (true)
{
// int data = rand() % 10 + 1;
// ringqueue->Push(data);
// pthread_mutex_lock(&mutex);
// cout << " 生产完成,生产数据 : " << data << endl;
// pthread_mutex_unlock(&mutex);
// sleep(1); // 生产慢一点,我慢了,你必须也慢下来 —— 典型的同步
// 构建or获取任务
int x = rand() % 10;
int y = rand() % 5;
char op = rand() % oper.size();
CalTask t(x, y, oper[op], my_math);
// 生产任务
ringqueue->Push(t);
printf("生产生产了一个任务 : %s\n", t.TaskToString().c_str());
// pthread_mutex_lock(&mutex);
// cout << " 生产生产了一个任务 : " << t.TaskToString() << endl;
// pthread_mutex_unlock(&mutex);
sleep(1);
}
}
void *ConsumerRoutine(void *args)
{
RingQueue<CalTask> *ringqueue = static_cast<RingQueue<CalTask> *>(args);
while (true)
{
// int data;
// ringqueue->Pop(&data);
// pthread_mutex_lock(&mutex);
// cout << " 消费完成,消费数据 : " << data << endl;
// pthread_mutex_unlock(&mutex);
CalTask t;
ringqueue->Pop(&t);
std::string result = t();
printf("消费消费了一个任务 : %s\n", result.c_str());
// pthread_mutex_lock(&mutex);
// cout << " 消费消费了一个任务 : " << result << endl;
// pthread_mutex_unlock(&mutex);
}
}
int main()
{
srand((unsigned int)time(nullptr) ^ getpid() ^ pthread_self());
RingQueue<CalTask> *rq = new RingQueue<CalTask>();
pthread_t p, c;
pthread_create(&p, nullptr, producerRoutine, rq);
pthread_create(&c, nullptr, ConsumerRoutine, rq);
pthread_join(p, nullptr);
pthread_join(c, nullptr);
delete rq;
return 0;
}
9-4-3、多生产多消费
我们上面的阻塞队列中的多生产多消费模型因为有锁,所以并发的时候,还是一个线程生产一个线程消费
所以,我们环形队列的多生产多消费并发场景,只需要保证 最终进入临界区的是一个生产一个消费就行!(加锁实现)
RingQueue.hpp:
#pragma once
#include <iostream>
#include <pthread.h>
#include <vector>
#include <cassert>
#include <functional>
#include <semaphore.h> //信号量所需的头文件
using std::cerr;
using std::cout;
using std::endl;
static const int gcap = 5; // 刚开始给的环形队列的最大空间
template <class T>
class RingQueue
{
public:
RingQueue(const int &cap = gcap)
: _queue(cap), // 环形队列初始化开辟cap个空间
_maxcap(cap)
{
int n = sem_init(&_spacesem, 0, _maxcap); // 初始化两个信号量
assert(0 == n);
n = sem_init(&_datasem, 0, 0);
assert(0 == n);
_ProducerStep = _ConsumerStep = 0; // 刚开始进入队列,生产者和消费者指向同一个位置
pthread_mutex_init(&_pmutex, nullptr);
pthread_mutex_init(&_cmutex, nullptr);
}
void Push(const T &in) // 不需要加锁
{
//pthread_mutex_lock(&_pmutex);加锁,保证生产与生产是互斥,消费与消费是互斥,生产与消费是同步切互斥
P(_spacesem); // 申请到了空间信号量 —— 能够进行正常的生产
pthread_mutex_lock(&_pmutex);
_queue[_ProducerStep++] = in;
_ProducerStep %= _maxcap; // 数组模拟环形队列
pthread_mutex_unlock(&_pmutex);
V(_datasem); // 生产者没有归还空间,因为数据占着空间了,但是我们多了数据,所以V数据信号量
//pthread_mutex_unlock(&_pmutex);
}
void Pop(T *out)
{
//pthread_mutex_lock(&_cmutex);
P(_datasem); // 申请数据信号量 —— 能够进行正常的消费
pthread_mutex_lock(&_cmutex);
*out = _queue[_ConsumerStep++];
_ConsumerStep %= _maxcap;
pthread_mutex_unlock(&_cmutex);
V(_spacesem); // 数据被消费了之后还了吗?没有,但是我们多了消费之后的空间,所以V空间信号量
//pthread_mutex_unlock(&_cmutex);
}
~RingQueue()
{
sem_destroy(&_spacesem);
sem_destroy(&_datasem);
pthread_mutex_destroy(&_pmutex);
pthread_mutex_destroy(&_cmutex);
}
private:
void P(sem_t &sem) // pv操作
{
int n = sem_wait(&sem);
// assert(0 == n);
// (void)n;
if (n != 0)
printf("P操作失败!\n");
}
void V(sem_t &sem)
{
int n = sem_post(&sem);
// assert(0 == n);
// (void)n;
if (n != 0)
printf("V操作失败!\n");
}
private:
std::vector<T> _queue; // 环形队列
int _maxcap; // 环形队列的最大容量
sem_t _spacesem; // 生产者看中的资源 —— 空间资源
sem_t _datasem; // 消费者看中的资源 —— 数据资源
int _ProducerStep; // 生产者在队列的位置
int _ConsumerStep; // 消费者在队列的位置
pthread_mutex_t _pmutex; // 生产者的锁
pthread_mutex_t _cmutex; // 消费者的锁
};
Task.hpp:
#pragma once
#include <iostream>
#include <queue>
#include <pthread.h>
#include <functional>
#include <cstdio>
#include <cstring>
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<int(int, int)> 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, 1024, "%d %c %d = ?", _x, _op, _y);
return buffer;
}
~CalTask()
{
}
private:
int _x;
int _y;
char _op;
func_t _callback;
};
main.cpp:
#include "RingQueue.hpp"
#include "Task.hpp"
#include <ctime> //时间戳需要
#include <cstdlib> //srand需要
#include <sys/types.h> //gerpid需要这两个头文件
#include <unistd.h>
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
std::string ThreadName() // 哪一个线程调用,就打印该线程的名字
{
char name[128];
snprintf(name, sizeof name, "thread[0x%x]", pthread_self());
return name;
}
void *producerRoutine(void *args)
{
RingQueue<CalTask> *ringqueue = static_cast<RingQueue<CalTask> *>(args);
while (true)
{
// int data = rand() % 10 + 1;
// ringqueue->Push(data);
// pthread_mutex_lock(&mutex);
// cout << " 生产完成,生产数据 : " << data << endl;
// pthread_mutex_unlock(&mutex);
// sleep(1); // 生产慢一点,我慢了,你必须也慢下来 —— 典型的同步
// 构建or获取任务
int x = rand() % 10;
int y = rand() % 5;
char op = rand() % oper.size();
CalTask t(x, y, oper[op], my_math);
// 生产任务
ringqueue->Push(t);
// printf("生产生产了一个任务 : %s\n", t.TaskToString().c_str());
pthread_mutex_lock(&mutex);
cout << ThreadName() << " 生产生产了一个任务 : " << t.TaskToString() << endl;
pthread_mutex_unlock(&mutex);
}
}
void *ConsumerRoutine(void *args)
{
RingQueue<CalTask> *ringqueue = static_cast<RingQueue<CalTask> *>(args);
while (true)
{
// int data;
// ringqueue->Pop(&data);
// pthread_mutex_lock(&mutex);
// cout << " 消费完成,消费数据 : " << data << endl;
// pthread_mutex_unlock(&mutex);
CalTask t;
ringqueue->Pop(&t);
std::string result = t();
// printf("消费消费了一个任务 : %s\n", result.c_str());
pthread_mutex_lock(&mutex);
cout << ThreadName() << " 消费消费了一个任务 : " << result << endl;
pthread_mutex_unlock(&mutex);
}
}
int main()
{
srand((unsigned int)time(nullptr) ^ getpid() ^ pthread_self());
RingQueue<CalTask> *rq = new RingQueue<CalTask>();
pthread_t p[4], c[8];
for (int i = 0; i < 4; ++i)
pthread_create(p + i, nullptr, producerRoutine, rq);
for (int i = 0; i < 8; ++i)
pthread_create(c + i, nullptr, ConsumerRoutine, rq);
for (int i = 0; i < 4; ++i)
pthread_join(p[i], nullptr);
for (int i = 0; i < 8; ++i)
pthread_join(c[i], nullptr);
delete rq;
return 0;
}
我们这个环形队列在多线程的前提下,有几个线程进入到环形队列了呢?
最少只有1个线程(一个生产或者一个消费),最多有两个线程(生产和消费同时在里面)。但是最多,只有一个生产和一个消费在里面!
那么,我们这里有4个生产,8个消费 —— 多生产多消费的意义是什么呢?
获取or构建任务是要花时间的!(磁盘外设,网络,数据库等地方拿数据)而消费任务/数据也是要花时间的!(调用非常耗时的处理方法)
未来,在获取or构建任务和消费任务要花费大量时间,我们可以通过多线程,给其他进程派发任务和处理其他任务,达到了 —— 获取or构建任务之前,消费任务之后,让线程并行的目的,这就大大节约了时间(不需要等你线程从而浪费获取or构建任务和消费任务的大量时间)
10、线程池
10-1、概念
前面我们学习过了基于管道的进程池,让不同的进程执行不同的代码
我们这里线程同样可以
-
线程池:
一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。 -
线程池的应用场景:
- 需要大量的线程来完成任务,且完成任务的时间比较短。 WEB服务器完成网页请求这样的任务,使用线程池技术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数。 但对于长时间的任务,比如一个Telnet连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了。
- 对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。
- 接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限,出现错误.
- 线程池的种类:
- 线程池示例:
- 创建固定数量线程池,循环从任务队列中获取任务对象,
- 获取到任务对象后,执行任务对象中的任务接口
这里的线程池和我们C++stl容器的扩容机制(空间配置器)很类似 —— 都属于池化技术
C++stl容器在空间不够的时候:
1、我们可以手动扩容
2、容器可以自动扩容
而扩容我们可以
1、分批进行申请多次,用多少申请多少,要用的时候再用
2、一次性直接申请完(在我们预估左右,不能用1000,申请1个亿!)
举个例子:
我们要1000个byte的空间,我们可以1、申请10次,每次申请100给byte
;2、一次性直接申请完1000byte
很明显,第二种一次性申请1000byte的方法更加高效,因为方法1多了9次申请内存的操作,底层就多了9次调用,还要通过页表映射等等操作,耗时比叫高!
池化技术 : 通过一定的成本,预先存储一批未来我们要使用到的数据,等到我们未来要使用这批数据的时候直接拿来使用就行!
所以,线程池也是一种池化技术,我们可以提前创建好一批线程,等我们拿到了数据/任务,这个时候线程再竞争,然后执行任务
10-2、代码
我们给线程池派发任务,然后就不用管了,线程池内部自己处理
在类内创建线程的回调函数,这个函数必须是static的,不然会有缺省参数,默认有this指针,而我们又不能传,导致我们不能用static的静态函数调用成员变量
我们这里直接把前面封装的线程和封装的锁直接拿过来使用
LockGuard.hpp(锁的封装,我们前面文件的名字叫Mutex.hpp这里改了个名字):
#pragma once
#include <iostream>
#include <pthread.h>
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;
};
Task.hpp(任务封装,获取or构建任务):
#pragma once
#include <iostream>
#include <queue>
#include <pthread.h>
#include <functional>
#include <cstdio>
#include <cstring>
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<int(int, int)> func_t;
public:
CalTask()
{
}
CalTask(int x, int y, char op, func_t func)
: _x(x),
_y(y),
_op(op),
_callback(func)
{
}
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;
}
std::string TaskToString()
{
char buffer[1024];
snprintf(buffer, 1024, "%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;
// };
Thread.hpp(线程的封装,代码上部分是以前的完整版本,下面是我们使用的简单版本):
#pragma once
#include <iostream>
#include <pthread.h>
#include <string>
#include <cstring>
#include <functional>
#include <cassert>
#include <unistd.h>
// class Thread;
// class Context // 上下文,将pthread_create中类内的第4个参数和this合并
// {
// public:
// Context()
// : _this(nullptr),
// _args(nullptr)
// {
// }
// ~Context()
// {
// }
// Thread *_this; // 调用函数的this指针(线程当前的对象)
// void *_args; // 线程当前执行函数的参数
// };
// class Thread
// {
// public:
// // using func_t = std::function<void*(void*)>;作用同下
// 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指针,第二个参数才是void* 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;//线程的tid
// func_t _func; // 线程未来执行的函数
// void *_args; // 线程执行函数的参数
// };
namespace my_thread
{
typedef std::function<void *(void *)> func_t;
const int num = 1024;
class Thread
{
// 在类内创建线程,想让线程执行对应的方法,需要将方法设置为static(静态方法) —— 因为static类内函数没有this指针!
static void *start_routine(void *args)
{
Thread *_this = static_cast<Thread *>(args);
return _this->callback();
}
public:
Thread()//构造不传参数
{
char namebuffer[num];
snprintf(namebuffer, sizeof namebuffer, "thread : %d", threadnum++);
_name = namebuffer;
}
void start(func_t func, void *args = nullptr)
{
_func = func;
_args = args;
int n = pthread_create(&_tid, nullptr, start_routine, this); // 这里直接传this指针
assert(0 == n);
(void)n;
}
// Thread(func_t func, void *args = nullptr) // 不要number了
// : _func(func),
// _args(args)
// {
// char namebuffer[num];
// snprintf(namebuffer, sizeof namebuffer, "thread : %d", threadnum++);
// _name = namebuffer;
// }
// void start()
// {
// int n = pthread_create(&_tid, nullptr, start_routine, this); // 这里直接传this指针
// assert(0 == n);
// (void)n;
// }
void join()
{
int n = pthread_join(_tid, nullptr);
assert(n == 0);
(void)n;
}
void *callback() // 不用传参数,回调函数自动调用类内成员变量 —— 下面的_args
{
return _func(_args); // 回调函数自动调用类内成员变量
}
std::string threadname() // 拿取线程名称
{
return _name;
}
~Thread()
{
}
private:
std::string _name; // 我们想直接看线程名字,比如线程1,线程2这种
pthread_t _tid; // 线程的tid
func_t _func; // 线程未来执行的函数
void *_args; // 线程执行函数的参数
static int threadnum; // 这里为了避免麻烦就不加锁了
};
int Thread::threadnum = 1;
}
Thread Pool.hpp(线程池实现的主要文件):
#pragma once
#include "Thread.hpp"
#include "LockGuard.hpp"
#include <vector>
#include <queue>
#include <pthread.h>
using namespace my_thread;
using std::cerr;
using std::cin;
using std::cout;
using std::endl;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
const int gnum = 5;
template <class T>
class ThreadPool;
template <class T>
class ThreadData
{
public:
ThreadData(ThreadPool<T> *tp, const std::string &name)
: _threadpool(tp),
_name(name)
{
}
~ThreadData()
{
}
public:
ThreadPool<T> *_threadpool;
std::string _name;
};
template <class T>
class ThreadPool
{
public: /定义成员函数
void mylock()
{
pthread_mutex_lock(&_mutex);
}
void myunlock()
{
pthread_mutex_unlock(&_mutex);
}
bool myQueueEmpty()
{
return _task_queue.empty();
}
void myThreadWait()
{
pthread_cond_wait(&_cond, &_mutex);
}
T Pop()
{
T t = _task_queue.front();
_task_queue.pop();
return t;
}
pthread_mutex_t *mutex() // 参与RAII
{
return &_mutex;
}
private:
static void *handlerTask(void *args) /可以使用C++里面的bind函数,这里就不使用了,免得增加负担
{
ThreadData<T> *td = static_cast<ThreadData<T> *>(args); // 直接使用对象指向函数的方法来达到目的
while (true)
{
T t; // 因为lockguard生命周期在{}里面 + 我们处理任务不在锁内,所以t要放到{}外面,不然t的生命周期也变成{}内,处理任务的时候看不到了
{ // 处理任务不需要在锁里面!!! —— 直接加个花括号就行!lockguard生命周期在{}里面
// td->_threadpool->mylock();
LockGuard lockguard(td->_threadpool->mutex()); // RAII —— 要构建临时对象!!!
while (td->_threadpool->myQueueEmpty())
{
td->_threadpool->myThreadWait();
}
t = td->_threadpool->Pop(); // pop的本质是将任务从公共队列(任务队列)中,拿到当前线程(多线程中任意一个线程)自己独立的栈中!!!
// td->_threadpool->myunlock();
}
/这个时候,多线程可以同时存在 —— 多个线程同时处理任务的并发状态!!!!!!!
cout << td->_name << " 获取了1个任务: " << t.TaskToString() << "处理完的结果是:" << t() << endl; // 线程拿到任务之后,这个任务就是私有的,不会被其他线程所访问到,所以处理任务放到锁外面没什么问题!!!
}
delete td;
return nullptr;
}
public:
ThreadPool(const int &num = gnum)
: _num(num)
{
pthread_mutex_init(&_mutex, nullptr);
pthread_cond_init(&_cond, nullptr);
for (size_t i = 0; i < _num; ++i)
{
_threads.push_back(new Thread()); // 构造不用传参数,下面的启动run函数传参!
}
}
void run()
{
for (const auto &t : _threads)
{
ThreadData<T> *td = new ThreadData<T>(this, t->threadname());
t->start(handlerTask, td);
printf("%s run... \n", t->threadname().c_str());
}
}
void Push(const T &in)
{
LockGuard lockguard(&_mutex); // RAII —— 要构建临时对象!!!
// pthread_mutex_lock(&_mutex);
_task_queue.push(in);
pthread_cond_signal(&_cond);
// pthread_mutex_unlock(&_mutex);
}
~ThreadPool()
{
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_cond);
for (const auto &t : _threads)
delete t; //_threads里面存放的是指针,直接delete指针就下
}
private:
int _num; // 线程个数
std::vector<Thread *> _threads; // 可以将Thread*优化为智能指针,就不需要我们手动释放了
std::queue<T> _task_queue; // 外面有很多"人"要在queue队列尾部生产任务,所以queue是一个公共资源,我们要对它进行叫锁保护
pthread_mutex_t _mutex;
pthread_cond_t _cond; // 多个线程没有任务就等,而有任务了就要唤醒 —— 使用条件变量
};
main.cc(测试文件):
#include "ThreadPool.hpp"
#include "Task.hpp"
#include <unistd.h>
#include <memory>
#include <unistd.h>
int main()
{
std::unique_ptr<ThreadPool<CalTask>> tp(new ThreadPool<CalTask>()); // 模板的类型<>不能忘记!
tp->run();
int x, y;
char op;
while (true)
{
cout << "请输入数据1#";
cin >> x;
cout << "请输入数据2#";
cin >> y;
cout << "请输入运算方法3#";
cin >> op;
CalTask t(x, y, op, my_math);
// cout << "你刚刚录入了一个任务: " << t.TaskToString() << " 确认提交吗?【y/n】#";
// char c;
// cin >> c;
// if (c == 'y')
tp->Push(t);
sleep(1);
}
return 0;
}
// #include "ThreadPool.hpp"///测试代码
// #include "Thread.hpp"
// #include <unistd.h>
// pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
// void *run(void *args)
// {
// std::string massage = static_cast<const char *>(args);
// while (true)
// {
// pthread_mutex_lock(&mutex);
// cout << massage << endl;
// pthread_mutex_unlock(&mutex);
// sleep(1);
// }
// }
// int main()
// {
// my_thread::Thread t1(run, (void *)"thread 1 run");
// my_thread::Thread t2(run, (void *)"thread 2 run");
// t1.start();
// t2.start();
// t1.join();
// t2.join();
// return 0;
// }
makefile文件:
threadpool:main.cpp
g++ -o threadpool main.cpp -std=c++11 -lpthread
。PHONY:clean
clean:
rm -rf threadpool
以上就是我们的线程池代码了,把以前我们封装的小工具都用上了,整体来说还是有一定难度的,要多动手写出来
11、线程安全的单例模式
什么是单例模式
单例模式是一种 "经典的, 常用的, 常考的设计模式
什么是设计模式
IT行业这么火, 涌入的人很多. 俗话说林子大了啥鸟都有. 大佬和菜鸡们两极分化的越来越严重. 为了让菜鸡们不太拖大佬的后腿, 于是大佬们针对一些经典的常见的场景, 给定了一些对应的解决方案, 这个就是设计模式
单例模式的特点
某些类, 只应该具有一个对象(实例), 就称之为单例
例如一个男人只能有一个媳妇.
在很多服务器开发场景中, 经常需要让服务器加载很多的数据 (上百G) 到内存中. 此时往往要用一个单例的类来管理这些数据
饿汉实现方式和懒汉实现方式
这两种模式是我们最常见的单例模式!
[洗完的例子]
吃完饭, 立刻洗碗, 这种就是饿汉方式. 因为下一顿吃的时候可以立刻拿着碗就能吃饭.
吃完饭, 先把碗放下, 然后下一顿饭用到这个碗了再洗碗, 就是懒汉方式.
懒汉方式最核心的思想是 “延时加载”. 从而能够优化服务器的启动速度
我们new和malloc开辟空间和懒汉方式是极其相似的!
我们new和malloc获取底层物理空间,并不会直接获取,而是先返回虚拟地址空间的地址
当我们真正用到new和malloc的空间的时候,这个时候因为我们拿到的虚拟地址空间的地址没有和物理地址映射,从而引发缺页中断,然后给我们重新进行通过页表将虚拟地址空间和物理地址映射,然后返回虚拟地址空间的地址,这个时候我们才是真正的new或者malloc出来了一段真正的空间!
所以,我们new和malloc并不是直接就给我们物理内存了(给我们的是虚拟地址的空间),而是当我们真正使用new或者malloc开辟的空间的时候,os才给我们真正的物理地址!
饿汉方式实现单例模式
template <typename T>
class Singleton
{
static T data;
public:
static T* GetInstance()
{
return &data;
}
};
只要通过 Singleton 这个包装类来使用 T 对象, 则一个进程中只有一个 T 对象的实例。立刻创建对象!
懒汉方式实现单例模式
template <typename T>
class Singleton
{
static T* inst;
public:
static T* GetInstance()
{
if (inst == NULL)
{
inst = new T();
}
return inst;
}
};
不会立刻创建对象,当我们需要用的时候,再创建对象!
接下来我们把我们上面的线程池改成懒汉方式,这里为了方便,我就只把改动的代码发出来,没改的代码,就去上面看看
ThreadPool.hpp:
template <class T>
class ThreadPool
{
private:
static void *handlerTask(void *args)
{
ThreadData<T> *td = static_cast<ThreadData<T> *>(args);
while (true)
{
T t;
{
LockGuard lockguard(td->_threadpool->mutex()); // RAII —— 要构建临时对象!!!
while (td->_threadpool->myQueueEmpty())
{
td->_threadpool->myThreadWait();
}
t = td->_threadpool->Pop();
}
cout << td->_name << " 获取了1个任务: " << t.TaskToString() << "处理完的结果是:" << t() << endl;
}
delete td;
return nullptr;
}
ThreadPool(const int &num = gnum) 构造函数要放到private里面 —————— 单例模式还有是个单例呢,不是没有例了!!!
: _num(num)
{
pthread_mutex_init(&_mutex, nullptr);
pthread_cond_init(&_cond, nullptr);
for (size_t i = 0; i < _num; ++i)
{
_threads.push_back(new Thread());
}
}
// 赋值拷贝还有许多地方要进行研究:支不支持连等,返回值是什么类型的,允不允许自己赋值自己
void operator=(const ThreadPool &) = delete; // 赋值语句要取消!!!!
// 拷贝构造没有void
ThreadPool(const ThreadPool &) = delete; // 拷贝构造要取消!!!!
public:
void run()
{
for (const auto &t : _threads)
{
ThreadData<T> *td = new ThreadData<T>(this, t->threadname());
t->start(handlerTask, td);
printf("%s run... \n", t->threadname().c_str());
}
}
void Push(const T &in)
{
LockGuard lockguard(&_mutex); // RAII —— 要构建临时对象!!!
_task_queue.push(in);
pthread_cond_signal(&_cond);
}
~ThreadPool()
{
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_cond);
for (const auto &t : _threads)
delete t;
}
///一般一个类内函数,既属于类又属于对象;我们这里加上static,使得函数只属于类,不属于对象!!!
static ThreadPool<T> *getInstance() // 单例模式这里可以传参的
{
if (tp == nullptr)
{
tp = new ThreadPool<T>();
}
return tp;
}
private:
int _num;
std::vector<Thread *> _threads;
std::queue<T> _task_queue;
pthread_mutex_t _mutex;
pthread_cond_t _cond;
static ThreadPool<T> *tp; // 要构建一个静态的指针
};
template <class T> // 静态指针tp的定义
ThreadPool<T> *ThreadPool<T>::tp = nullptr;
main.cpp:
int main()
{
ThreadPool<CalTask>::getInstance()->run(); // 在创建对象的函数前面加上static就不会报错了
// std::unique_ptr<ThreadPool<CalTask>> tp(new ThreadPool<CalTask>()); // 模板的类型<>不能忘记!
// tp->run();
int x, y;
char op;
while (true)
{
cout << "请输入数据1#";
cin >> x;
cout << "请输入数据2#";
cin >> y;
cout << "请输入运算方法3#";
cin >> op;
CalTask t(x, y, op, my_math);
// cout << "你刚刚录入了一个任务: " << t.TaskToString() << " 确认提交吗?【y/n】#";
// char c;
// cin >> c;
// if (c == 'y')
// tp->Push(t);
ThreadPool<CalTask>::getInstance()->Push();
sleep(1);
}
return 0;
}
但是上面的懒汉模式还是有一点问题:tp是一个static静态的指针,那么所有人就都可以访问这个tp,所以tp是一个不安全的指针!
改进::
~ThreadPool()
{
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_cond);
for (const auto &t : _threads)
delete t;
}
///一般一个类内函数,既属于类又属于对象;我们这里加上static,使得函数只属于类,不属于对象!!!
static ThreadPool<T> *getInstance() // 单例模式这里可以传参的
{
if (tp == nullptr)//增加并发度,不要每一次进来都直接面对申请锁
{
_singlock.lock();
if (tp == nullptr)
{
tp = new ThreadPool<T>();
}
_singlock.unlock();
}
return tp;
}
private:
int _num;
std::vector<Thread *> _threads;
std::queue<T> _task_queue;
pthread_mutex_t _mutex;
pthread_cond_t _cond;
static ThreadPool<T> *tp; // 要构建一个静态的指针
static std::mutex _singlock;
};
//只有定义了对象,模板才会实例化!
template <class T> // 静态指针tp的定义
ThreadPool<T>* ThreadPool<T>::tp = nullptr;
//我们ThreadPool<T>类型不明确,但是ThreadPool<T>*是一个指针类型,这个ThreadPool<T>*是明确的!
template <class T>
std::mutex ThreadPool<T>::_singlock;
注意事项:
- 加锁解锁的位置
- 双重 if 判定, 避免不必要的锁竞争
- volatile关键字防止过度优化
// 懒汉模式, 线程安全
template <typename T>
class Singleton
{
volatile static T* inst; // 需要设置 volatile 关键字, 否则可能被编译器优化.
static std::mutex lock;
public:
static T* GetInstance()
{
if (inst == NULL)
{ // 双重判定空指针, 降低锁冲突的概率, 提高性能.
lock.lock(); // 使用互斥锁, 保证多线程情况下也只调用一次 new.
if (inst == NULL)
{
inst = new T();
}
lock.unlock();
}
return inst;
}
};
我们定义的这个static静态指针,有可能被编译器优化到寄存器里面去了,我们多个线程使用的时候都是从寄存器拿的!
我们多个线程都访问这个static静态指针,可以能有的线程将这个static静态指针的值改了,但是我们还是访问的寄存器里面的静态指针的值,就出问题了!
12、 STL,智能指针和线程安全
STL中的容器是否是线程安全的?
不是.
原因是, STL 的设计初衷是将性能挖掘到极致, 而一旦涉及到加锁保证线程安全, 会对性能造成巨大影响.而且对于不同的容器, 加锁方式的不同, 性能可能也不同(例如hash表的锁表和锁桶).因此 STL 默认不是线程安全. 如果需要在多线程环境下使用, 往往需要调用者自行保证线程安全.
智能指针是否是线程安全的?
对于 unique_ptr, 由于只是在当前代码块范围内生效, 因此不涉及线程安全问题.
对于 shared_ptr, 多个对象需要共用一个引用计数变量, 所以会存在线程安全问题. 但是标准库实现的时候考虑到了这个问题, 基于原子操作(CAS)的方式保证 shared_ptr 能够高效, 原子的操作引用计数.
13、其他常见的各种锁
1、悲观锁:在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁,写锁,行锁等),当其他线程想要访问数据时,被阻塞挂起。
2、乐观锁:每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据前,会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式:版本号机制和CAS操作。
3、CAS操作:当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不等则失败,失败则重试,一般是一个自旋的过程,即不断重试。
4、自旋锁,公平锁,非公平锁?这些就靠大家去查找了
13-1、自旋锁
我们上面学习的互斥锁、信号量是一种挂起等待锁
场景1:线程B看到线程A在使用锁,并且A要占有锁要占有很长的时间,所以B会将自己挂起,然后到特定的地点进行等待(挂起等待),等到A释放锁了再被唤醒
场景2:线程B看到线程A在使用锁,B一直问A,你有没有使用完锁,A回答没有一直用着锁,这就是自旋(和轮询大差不差)
那么,是什么决定了等待方式呢?(什么决定了锁是一个挂起等待锁,还是一个自旋锁)
我们要等待的时间长短问题!
但是,我们要怎么评估时间的长短呢?(什么情况用自旋锁,什么情况用挂起等待锁)
这个是没有标准答案的!时间的长短是通过对比出来的,参照物不同时间的长短就不同
比如说:临界区里面有许多io或者复杂操作,就需要用到挂起等待锁;但是如果临界区里面是我们上面的抢票等代码很短很快就可以使用自旋锁
使用什么锁需要结合实际场景来决定(大部分情况下都是挂起等待锁,因为挂起等待锁就算死锁了,顶多就是线程之间互相不推进,不向下执行代码了。但是自旋锁我们评估时间失误或者其他情况导致死锁,那么所有执行流就会疯狂自选检查锁但是又不释放,导致cpu很快被占用满了)
除非我们遇到了非常适合自旋锁的场景,并且保证自旋锁不会出问题才会使用!
还有一种方法:就算我们把自旋锁和挂起等待锁都使用,测试谁效果更好就用谁
14、. 读者写者问题
读写锁在编写多线程的时候,有一种情况是十分常见的。那就是,有些公共数据修改的机会比较少。相比较改写,它们读的机会反而高的多。通常而言,在读的过程中,往往伴随着查找的操作,中间耗时很长。给这种代码段加锁,会极大地降低我们程序的效率。那么有没有一种方法,可以专门处理这种多读少写的情况呢? 有,那就是读写锁。
注意:写独占,读共享,读锁优先级高
我们有的时候数据还没有写完,就有人来读了,这个时候数据因为没有写完,读到的都是缺失数据,结果不正确,所以写的时候,不能有人来读(写的时候,一个人写,读的时候,一群人来读)
所以写独占,读共享
321原则:
3种关系:
读者和读者:没有关系!!!
写者和写者(互斥):我写完了,你才能写,不然我写了你把我写的覆盖了,反过来我把你覆盖了都不行!
读着和写者(互斥和同步):写者没写完读者不能来读,读者读的时候,写者不能写(互斥);写者写了1年都没人来读,或者读者读了1年的数据都是一样的,这个时候写者就要更新写入数据(同步)
2个角色:
读者和写者
1个场景:
交易场所(一段特定结构的缓冲区)
读者写者模型 VS 生产消费模型的本质区别是:消费者会拿走数据,而读者不会拿走数据
读者写者模型场景:一次发布,很长时间不做修改,大部分时间都是读取,少量时间被写入 —— 读者多,写者少!(写博客、媒体新闻)
设置读写优先
int pthread_rwlockattr_setkind_np(pthread_rwlockattr_t *attr, int pref);
/*
pref 共有 3 种选择
PTHREAD_RWLOCK_PREFER_READER_NP (默认设置) 读者优先,可能会导致写者饥饿情况
PTHREAD_RWLOCK_PREFER_WRITER_NP 写者优先,目前有 BUG,导致表现行为和
PTHREAD_RWLOCK_PREFER_READER_NP 一致
PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP 写者优先,但写者不能递归加锁
*/
初始化
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t *restrict attr);
销毁
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
加锁和解锁
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);//读加锁
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);//写加锁
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);//解锁
在任何时刻,只允许一个写者写入,但是允许多个读者读取!
unlock是怎么解锁的呢?
举例 : pthread_rwlock_t 这个结构体里面有读锁和写锁,unlock只要在对应结构体里面将读锁/写锁/两个锁全部释放就行!
如果读者特别特别多,写者一直写不了,就出现了 —— 写者饥饿问题!
这是正常现象,因为读写者模型就算读者多,写者少
写独占,读共享,读锁优先级高
我们也有写者优先的场景!
比如 :读者有10个,写者1个,有5个读者在写者前面,还有5个读者在写者后面。本来是10个读者读完写者再写。
但是,写者优先就是后面5个读者给我等着,按顺序来,我写者是第6个,我写完了你们后面5个再读!
下面是一份读写锁的测试代码:
#include <vector>
#include <sstream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <pthread.h>
volatile int ticket = 1000;
pthread_rwlock_t rwlock;
void *reader(void *arg)
{
char *id = (char *)arg;
while (1)
{
pthread_rwlock_rdlock(&rwlock);
if (ticket <= 0)
{
pthread_rwlock_unlock(&rwlock);
break;
}
printf("%s: %d\n", id, ticket);
pthread_rwlock_unlock(&rwlock);
usleep(1);
}
return nullptr;
}
void *writer(void *arg)
{
char *id = (char *)arg;
while (1)
{
pthread_rwlock_wrlock(&rwlock);
if (ticket <= 0)
{
pthread_rwlock_unlock(&rwlock);
break;
}
printf("%s: %d\n", id, --ticket);
pthread_rwlock_unlock(&rwlock);
usleep(1);
}
return nullptr;
}
struct ThreadAttr
{
pthread_t tid;
std::string id;
};
std::string create_reader_id(std::size_t i)
{
// 利用 ostringstream 进行 string 拼接
std::ostringstream oss("thread reader ", std::ios_base::ate);
oss << i;
return oss.str();
}
std::string create_writer_id(std::size_t i)
{
// 利用 ostringstream 进行 string 拼接
std::ostringstream oss("thread writer ", std::ios_base::ate);
oss << i;
return oss.str();
}
void init_readers(std::vector<ThreadAttr> &vec)
{
for (std::size_t i = 0; i < vec.size(); ++i)
{
vec[i].id = create_reader_id(i);
pthread_create(&vec[i].tid, nullptr, reader, (void *)vec[i].id.c_str());
}
}
void init_writers(std::vector<ThreadAttr> &vec)
{
for (std::size_t i = 0; i < vec.size(); ++i)
{
vec[i].id = create_writer_id(i);
pthread_create(&vec[i].tid, nullptr, writer, (void *)vec[i].id.c_str());
}
}
void join_threads(std::vector<ThreadAttr> const &vec)
{
// 我们按创建的 逆序 来进行线程的回收
for (std::vector<ThreadAttr>::const_reverse_iterator it = vec.rbegin(); it !=
vec.rend();
++it)
{
pthread_t const &tid = it->tid;
pthread_join(tid, nullptr);
}
}
void init_rwlock()
{
#if 0
// 写优先
pthread_rwlockattr_t attr;
pthread_rwlockattr_init(&attr);
pthread_rwlockattr_setkind_np(&attr, PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP);
pthread_rwlock_init(&rwlock, &attr);
pthread_rwlockattr_destroy(&attr);
#else
// 读优先,会造成写饥饿
pthread_rwlock_init(&rwlock, nullptr);
#endif
}
int main()
{
// 测试效果不明显的情况下,可以加大 reader_nr
// 但也不能太大,超过一定阈值后系统就调度不了主线程了
const std::size_t reader_nr = 1000;
const std::size_t writer_nr = 2;
std::vector<ThreadAttr> readers(reader_nr);
std::vector<ThreadAttr> writers(writer_nr);
init_rwlock();
init_readers(readers);
init_writers(writers);
join_threads(writers);
join_threads(readers);
pthread_rwlock_destroy(&rwlock);
}
15 、总结
本节内容非常多,且有很多重点知识,所以需要把代码等知识多过几遍,深入理解!
系统——文件——内存
我们内存学的比较少,未来需要学习一下内存方面的知识,面试有可能被问到!