C++【多线程】
阿里云国内75折 回扣 微信号:monov8 |
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6 |
文章目录
一、什么是线程
线程在进程内部执行是OS调度的基本单位。
在堆区上存在下面一种数据结构
struct vm_area_struct{
//用来记录这块空间的起始和终止。
unsigned long vm_start;
unsigned long vm_end;
//其实这是一个双向链表中的结点用具记录前后的空间节点
struct vm_ares_struct *vm_next ,*vm_prev;
}
如果我们的堆区申请了比较多的空间然后我们的vm_area_struct就是用来记录
我们每一小块的地址空间的起始和结束。
然后这些小的内存块就通过双向链表的形式串联起来。
所以说OS是可以做到让进程进行细粒度的划分的
用户级页表+MMU是集成在CPU当中的
我们如何从虚拟地址映射到物理地址
1.exe就是一个文件
2.我们的可执行程序本来就是按照地址空间方式进行编译的
3.可执行程序其实按照区域也已经被划分成了以4kb为单位的空间。
我们如何管理这里的每一个4kb的空间呢
我们需要先描述再组织也就是用struct page结构体来进行描述
struct page
{
int flag;
}
内核想要管理这么多物理内存我们就需要创建一个数组struct page mem[100w+]
然后操作系统想要管理对应的物理内存的时候就可以通过这一个数组进行管理。
所以操作系统对于物理内存的管理就变成了对于对应的数据结构的管理。
磁盘中的可执行文件是按照4kb划分的我们的物理内存也是按照4kb划分的其中我们将磁盘当中以4kb为单位的我们的代码的数据的内容称之为页帧
我们物理内存这里的4kb大小称之为页框
IO的基本单位是4kbIO就是将页帧装进页框里
缺页中断如果我们的操作系统在寻值得时候发现对应的数据不在我们的内存中我们就需要去磁盘中读取对应的数据到我们的内存中然后通过页表映射获取到我们的数据。
我们的虚拟地址有232个4GB页表是保存在物理内存当中的也就是说如果想要保存我们的一整张页表的话需要的大小为页表的条目的大小×4GB这样空间占用就会非常大。
但是我们可以按照下图建立一级页表和二级页表来简化我们的索引。
如何理解线程
通过我们创建了多个task_strcu纸箱同一个mm_struct通过一定的技术手段
将当前进程的“资源”以一定的方式划分给不同的task_struct
也就是说我们再创建task_struct的时候不再去开辟新的资源了。
我们就将这里的每一个task_struct就称为线程。
什么是线程在进程内部执行
线程在进程的地址空间内进行运行。
为什么线程是0S调度的基本单位
因为cpu并不关心执行流是线程还是进程只关心pcb。
这只是Linux下的维护方案没有为线程设计专门的数据结构。
但只要比进程更轻量粒度更轻就是线程。
windows有为线程设计专门的数据结构。
什么是进程资源角度
进程就是我们对应的内核数据结构再加上该进程所对应的代码和数据。
一个进程可能会有多个PCB。
在内核的时间进程是承担系统分配资源的基本实体。
所以我们创建线程的时候只有第一个需要申请资源也就是我们上面图中红框的那一个task_struct也就是一个进程后面所创建的线程不是想操作系统索要资源而是向我们的进程共享了资源。
如何理解我们曾经我们所写的所有的代码
内部只有一个执行流的进程。
我们现在就可以创建内部具有多个执行流的进程。
我们的task_struct仅仅是我们的进程内部的一个执行流。
在CPU的视角CPU其实不怎么关心当前是进程还是线程这样的概念只人stask_struct。
我们的CPU的调度其实调度的是stack_struct
在Linux下PCB<=其他操作系统的PCB的
Linux下的进程统一称之为轻量级进程。
当CPU拿到一个PCB的时候可以是单执行流的进程的PCB也可可能是多执行流的其中一个线程的PCB所以比那些别的操作系统单独给线程和进程设计的数据结构更加轻量
所以Linux没有真正意义上的线程结构Linux上是用进程PCB模拟线程的。
所以Linux并不能直接给我们提供线程相关的接口只能提供轻量级进程的接口在用户层实现了一套用户层多线程方案以库的方式提供给其他用户进行使用pthread线程库–原生线程库。
线程如何看待进程内部的资源呢
原则向线程能够看到进程的所有资源在进程的上下文中进行操作。
进程 vs 线程
调度层面上下文调度一个线程的成本比调度进程的成本更低
线程的优点
创建一个新线程的代价要比创建一个新进程小得多
与进程之间的切换相比线程之间的切换需要操作系统做的工作要少很多
线程占用的资源要比进程少很多
能充分利用多处理器的可并行数量
在等待慢速I/O操作结束的同时程序可执行其他的计算任务
计算密集型应用为了能在多处理器系统上运行将计算分解到多个线程中实现
I/O密集型应用为了提高性能将I/O操作重叠。线程可以同时等待不同的I/O操作。
线程的缺点
- 性能损失
一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多那么可能会有较大的性能损失这里的性能损失指的是增加了额外的同步和调度开销而可用的资源不变。 - 健壮性降低
编写多线程需要更全面更深入的考虑在一个多线程程序里因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的换句话说线程之间是缺乏保护的。 - 缺乏访问控制
进程是访问控制的基本粒度在一个线程中调用某些OS函数会对整个进程造成影响。 - 编程难度提高
编写与调试一个多线程程序比单线程程序困难得多
线程是不是越多越好
线程越多线程之间的切换也回更加频繁这会导致系统的开销变大导致我们的效率反而下降。一般我们创建线程的数量等于CPU的核心数。
二、创建线程
pthread_create
我们还需要在我们的makefile中添加-lpthead选项
mythread:mythread.cc
g++ -o mythread mythread.cc -std=c++11 -lpthread
.PHONY:clean
clean:
rm -f mythread
#include<iostream>
#include<pthread.h>
#include<string>
#include<cstdio>
#include<unistd.h>
using namespace std;
void *threadRun(void *args)
{
const string name=(char*)args;
while(true)
{
//如果线程属于进程的话我们这里获得的pid应该和我们的线程是同一个pid
cout<<name<<", pid: "<<getpid()<<endl;
cout<<endl;
sleep(1);
}
}
int main()
{
//无符号长整数类型
pthread_t tid[5];
char name[64];
//循环创建5个线程
for(int i=0;i<5;i++)
{
//格式化我们线程的名字
snprintf(name,sizeof name,"%s-%d","thread",i);
pthread_create(tid+i,nullptr,threadRun,(void *)name);
sleep(1);//缓解传参的bug
}
//我们的主线程在执行完上面的代码之后就会执行下面的代码。
while(true)
{
cout<<"main thread, pid: "<<getpid()<<endl;
sleep(3);
}
}
我们这里看我们的程序已经链接上了我们的pthread_create库
但是我们再这里只能查看到到一个进程我们如何查看到这个进程里面的线程呢
编写监控脚本
ps -aL |head |head -1 && ps -aL| grep mythread
我们Linux内部所看的一定是LWP不是看的PID。
如果只是单线程的话这个进程的PID和LWP是相同的。
我们这里只要将我们的进程终止了我们所有的线程都会终止。
因为我们现成的所以资源都是来自于我们的进程的没有了代码和数据当然会退出。
线程的共享资源
文件描述符表
每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
当前工作目录
用户id和组id
堆区可以被共享
共享区也是被所有线程共享的
栈区也是可以共享的但我们一般不这么做。
线程的私有资源
线程ID
一组寄存器线程的上下文
栈
errno
信号屏蔽字
调度优先级
进程和线程切换我们为什么说线程的切换成本更低
如果我们调度的一个进程内的若干个线程我们的地址空间不需要切换页表也不需要切换
如果是进程切换的话地址空间页表等等都需要切换。
并且我们的CPU内是有硬件级别的缓存的cacheL1-L3
我们只要将相关的数据load到我们CPU内部的缓存对内存的代码和数据根据局部性原理
一条指令如果被使用了它附近的代码也有很大的可能被使用预读取到我们的CPU的缓存中
这样我们的CPU就不需要访问内存直接到缓存中访问就可以了。
但是如果进程切换那么我们的cache立即失效新进程过来的时侯只能重新缓存。
所以我们的线程切换比我们的进程切换更加轻量化。
#include<iostream>
#include<pthread.h>
#include<string>
#include<cstdio>
#include<unistd.h>
using namespace std;
void *threadRoutine(void *args)
{
while(true)
{
cout<<"新线程: "<<(char*)args<<"running...."<<endl;
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid ,nullptr,threadRoutine,(void*)"thread 1");
while(true)
{
cout<<"main线程: "<<"running...."<<endl;
sleep(1);
}
return 0;
}
如果我们让新线程异常我们的一整个进程都会因为这个异常而退出
#include<iostream>
#include<pthread.h>
#include<string>
#include<cstdio>
#include<unistd.h>
using namespace std;
void *threadRoutine(void *args)
{
while(true)
{
cout<<"新线程: "<<(char*)args<<"running...."<<endl;
sleep(1);
int a=100;
a/=0;
}
}
int main()
{
pthread_t tid;
pthread_create(&tid ,nullptr,threadRoutine,(void*)"thread 1");
while(true)
{
cout<<"main线程: "<<"running...."<<endl;
sleep(1);
}
return 0;
}
线程在创建并执行的时候线程也是需要进行等待的。
如果主线程不等待就回引起类似于进程的僵尸问题导致内存泄漏。
pthread_join
pthread_join是用来进行线程等待的。
#include<iostream>
#include<pthread.h>
#include<string>
#include<cstdio>
#include<unistd.h>
using namespace std;
void *threadRoutine(void *args)
{
int i=0;
while(true)
{
cout<<"新线程: "<<(char*)args<<"running...."<<endl;
sleep(1);
if(i++==10)
{
break;
}
}
}
int main()
{
pthread_t tid;
pthread_create(&tid ,nullptr,threadRoutine,(void*)"thread 1");
pthread_join(tid,nullptr);//默认会阻塞等待新线程的退出。
cout<<"main thread wait done"<<endl;
while(true)
{
cout<<"main线程: "<<"running...."<<endl;
sleep(1);
}
return 0;
}
新线程的返回值返回给谁呢
一般是给主线程main_threadmain如何获取到呢
int pthread_join(pthread_t thread, void **retval);
#include<iostream>
#include<pthread.h>
#include<string>
#include<cstdio>
#include<unistd.h>
using namespace std;
void *threadRoutine(void *args)
{
int i=0;
while(true)
{
cout<<"新线程: "<<(char*)args<<"running...."<<endl;
sleep(1);
if(i++==10)
{
break;
}
}
cout<<"new thread quit ..."<<endl;
return (void*)10;//是返回给谁呢一般是给主线程?pthread_join
}
int main()
{
pthread_t tid;
pthread_create(&tid ,nullptr,threadRoutine,(void*)"thread 1");
//指针就是一个数据是一个字面量可以保存到指针变量中
//这是一个指针变量可以用于承装我们的数据
void *ret=nullptr;
pthread_join(tid,&ret);//默认会阻塞等待新线程的退出。
cout<<"main thread wait done .. main quit ... :new thread quit"<<(long long)ret<<endl;//64位下这里是long long32位下是int
while(true)
{
cout<<"main线程: "<<"running...."<<endl;
sleep(1);
}
return 0;
}
多线程可以再新线程和主线程之间传递信息
这里我们可以传递一整个数组或者别的数据
#include<iostream>
#include<pthread.h>
#include<string>
#include<cstdio>
#include<unistd.h>
using namespace std;
void *threadRoutine(void *args)
{
int i=0;
int *data =new int[10];
while(true)
{
data[i]=i;
cout<<"新线程: "<<(char*)args<<"running...."<<endl;
sleep(1);
if(i++==10)
{
break;
}
}
cout<<"new thread quit ..."<<endl;
return (void*)data;
}
int main()
{
pthread_t tid;
pthread_create(&tid ,nullptr,threadRoutine,(void*)"thread 1");
//指针就是一个数据是一个字面量可以保存到指针变量中
//这是一个指针变量可以用于承装我们的数据
int *ret=nullptr;
pthread_join(tid,(void**)&ret);//默认会阻塞等待新线程的退出。
cout<<"main thread wait done .. main quit ... :new thread quit"<<endl;//64位下这里是long long32位下是int
for(int i=0;i<10;i++)
{
cout<<ret[i]<<endl;
}
return 0;
}
我们的主进程为什么没有获取新线程的退出码之类的接口
一个线程崩了整一个进程就崩掉了获取退出码没有意义。
1.线程谁先运行与调度器有关
2.线程一旦异常都可能导致整个进程整体退出
3.现成的输入和返回值问题
4.线程异常退出的理解
三、线程退出
pthread_exit
在多线程中不要调用exit
exit是用来终止进程的。
#include<iostream>
#include<pthread.h>
#include<string>
#include<cstdio>
#include<unistd.h>
using namespace std;
void *threadRoutine(void *args)
{
int i=0;
int *data =new int[10];
while(true)
{
data[i]=i;
cout<<"新线程: "<<(char*)args<<"running...."<<endl;
sleep(1);
if(i++==3)
{
break;
}
}
exit(10);
cout<<"new thread quit ..."<<endl;
// return (void*)data;
}
int main()
{
pthread_t tid;
pthread_create(&tid ,nullptr,threadRoutine,(void*)"thread 1");
//指针就是一个数据是一个字面量可以保存到指针变量中
//这是一个指针变量可以用于承装我们的数据
int *ret=nullptr;
pthread_join(tid,(void**)&ret);//默认会阻塞等待新线程的退出。
//我们下面这句话就没有办法打印出来
cout<<"main thread wait done .. main quit ... :new thread quit"<<endl;//64位下这里是long long32位下是int
return 0;
}
这样调用的话我们的 main thread wait done … main quit … :new thread quit"这句话就没办法打印出来也就是说在quit之后我们一整个进程就全部退出了别的线程后面的代码根本就没办法执行。
所以我们使用pthread_exit()
#include<iostream>
#include<pthread.h>
#include<string>
#include<cstdio>
#include<unistd.h>
using namespace std;
void *threadRoutine(void *args)
{
int i=0;
int *data =new int[10];
while(true)
{
data[i]=i;
cout<<"新线程: "<<(char*)args<<"running...."<<endl;
sleep(1);
if(i++==3)
{
break;
}
}
pthread_exit((void*)10);
cout<<"new thread quit ..."<<endl;
// return (void*)data;
}
int main()
{
pthread_t tid;
pthread_create(&tid ,nullptr,threadRoutine,(void*)"thread 1");
//指针就是一个数据是一个字面量可以保存到指针变量中
//这是一个指针变量可以用于承装我们的数据
int *ret=nullptr;
pthread_join(tid,(void**)&ret);//默认会阻塞等待新线程的退出。
cout<<"main thread wait done .. main quit ... :new thread quit"<<(long long)ret<<endl;//64位下这里是long long32位下是int
return 0;
}
pthread_cancel
#include<iostream>
#include<pthread.h>
#include<string>
#include<cstdio>
#include<unistd.h>
using namespace std;
void *threadRoutine(void *args)
{
int i=0;
while(true)
{
cout<<"新线程: "<<(char*)args<<"running...."<<endl;
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid ,nullptr,threadRoutine,(void*)"thread 1");
int count=0;
while(true)
{
cout<<"main线程: "<<"running...."<<endl;
sleep(1);
count++;
if(count>=5) break;
}
pthread_cancel(tid);
cout<<"pthread cancel: "<<tid<<endl;
int *ret=nullptr;
pthread_join(tid,(void**)&ret);//默认会阻塞等待新线程的退出。
cout<<"main thread wait done .. main quit ... :new thread quit"<<(long long)ret<<endl;//64位下这里是long long32位下是int
return 0;
}
1.线程被取消join的时候退出码是-1 #define PTHREAD_CANCELED((void*)-1)
主线程可以取消新线程新线程可不可以处理掉主线程
一般不这么做一般都是用主线程去等待新线程的。不然我们的主线程的相关的信息没线程处理了。
线程id
#include<iostream>
#include<pthread.h>
#include<string>
#include<cstdio>
#include<unistd.h>
using namespace std;
void *threadRoutine(void *args)
{
int i=0;
while(true)
{
cout<<"新线程: "<<(char*)args<<"running...."<<endl;
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid ,nullptr,threadRoutine,(void*)"thread 1");
printf("%lu,%p\n",tid,tid);
int count=0;
while(true)
{
cout<<"main线程: "<<"running...."<<endl;
sleep(1);
count++;
if(count>=5) break;
}
pthread_cancel(tid);
cout<<"pthread cancel: "<<tid<<endl;
int *ret=nullptr;
pthread_join(tid,(void**)&ret);//默认会阻塞等待新线程的退出。
cout<<"main thread wait done .. main quit ... :new thread quit"<<(long long)ret<<endl;//64位下这里是long long32位下是int
return 0;
}
这两个非常大的整数就是我们的线程id
这个tid是我们的线程id本质上是一个地址
因为我们目前用的不是Linux自带的常见线程的借口不是操作系统的接口
而是pthread库中的接口。
所以用户和操作系统之间就有一个软件层pthread库。
这里的线程也需要管理起来操作系统需要负责调度方面这个库需要提供用户方面的借口和相关的字段对应的栈结构。
想要在用户层对我们的线程进行管理的话我们就需要再thread库中进行管理
多线程如何保证每一个线程独占栈区呢
在用户层尽量会给提供
这个线程在我们的库中的相关属性的起始地址就是tid.
所以线程id就是一个地址。
我们主线程用的是内核级栈结构我们的新线程用的就是库中的私有栈结构用共享区地址来充当栈结构。
pthread_self
获取自己的线程id
#include<iostream>
#include<pthread.h>
#include<string>
#include<cstdio>
#include<unistd.h>
using namespace std;
void *threadRoutine(void *args)
{
int i=0;
while(true)
{
cout<<"新线程: "<<(char*)args<<"running...."<<pthread_self()<<endl;
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid ,nullptr,threadRoutine,(void*)"thread 1");
// printf("%lu,%p\n",tid,tid);
int count=0;
while(true)
{
cout<<"main线程: "<<"running.... main tid:"<<pthread_self()<<endl;
sleep(1);
count++;
if(count>=5) break;
}
pthread_cancel(tid);
cout<<"pthread cancel: "<<tid<<endl;
int *ret=nullptr;
pthread_join(tid,(void**)&ret);//默认会阻塞等待新线程的退出。
cout<<"main thread wait done .. main quit ... :new thread quit"<<(long long)ret<<endl;//64位下这里是long long32位下是int
return 0;
}
那我们是不是线程可以自己取消自己
pthread_cancel(pthread_self())
可以但是尽量不要这么做。
四、进程对于共享资源的访问
全局变量被多个线程共享
#include<iostream>
#include<pthread.h>
#include<string>
#include<cstdio>
#include<unistd.h>
using namespace std;
int g_val=0;
void *threadRoutine(void *args)
{
while(true)
{
cout<<(char*)args<<" : "<<g_val<<" &: "<<&g_val <<endl;
g_val++;
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid ,nullptr,threadRoutine,(void*)"thread 1");
while(true)
{
cout<<"main thread : "<<g_val<<" &: "<<&g_val <<endl;
sleep(1);
}
return 0;
}
__thread
__thread修饰全局变量带来的结构就是让每一个线程各自拥有一个全局变量–现成的局部存储
也就是你每创建一个线程这个全局变量就会给你拷贝几份。
#include<iostream>
#include<pthread.h>
#include<string>
#include<cstdio>
#include<unistd.h>
using namespace std;
__thread int g_val=0;
void *threadRoutine(void *args)
{
while(true)
{
cout<<(char*)args<<" : "<<g_val<<" &: "<<&g_val <<endl;
g_val++;
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid ,nullptr,threadRoutine,(void*)"thread 1");
while(true)
{
cout<<"main thread : "<<g_val<<" &: "<<&g_val <<endl;
sleep(1);
}
return 0;
}
如果我们再线程内部进行程序替换会发生什么
会将一整个进程的代码和数据全部都替换掉别的线程的代码也会被替换掉。
这里我们在新线程当中进行了进程替换但是我们的主线程也被替换掉了。
#include<iostream>
#include<pthread.h>
#include<string>
#include<cstdio>
#include<unistd.h>
using namespace std;
__thread int g_val=0;
void *threadRoutine(void *args)
{
sleep(5);
execl("/bin/ls","ls",nullptr);
while(true)
{
cout<<(char*)args<<" : "<<g_val<<" &: "<<&g_val <<endl;
g_val++;
sleep(1);
}
}
int main()
{
pthread_t tid;
pthread_create(&tid ,nullptr,threadRoutine,(void*)"thread 1");
while(true)
{
cout<<"main thread : "<<g_val<<" &: "<<&g_val <<endl;
sleep(1);
}
return 0;
}
也就是除了主线程之外别的线程都不运行了我们主线程的接下来的代码被替换成了ls指令。
五、分离线程
如果我不关心我线程的返回值我只是想创建一个线程。这个线程在退出了之后自动释放线程资源。我们的主线程不需要去join了新线程的资源会被库进行回收
pthread_detach
分离线程一般是线程自己分离自己
分离之后就不能使用join了如果我们想要强制使用join呢
#include<iostream>
#include<pthread.h>
#include<string>
#include<cstdio>
#include<cstring>
#include<unistd.h>
#include<errno.h>
using namespace std;
__thread int g_val=0;
void *threadRoutine(void *args)
{
pthread_detach(pthread_self());
sleep(5);
execl("/bin/ls","ls",nullptr);
while(true)
{
cout<<(char*)args<<" : "<<g_val<<" &: "<<&g_val <<endl;
g_val++;
sleep(1);
}
pthread_exit((void*)11);
}
int main()
{
pthread_t tid;
pthread_create(&tid ,nullptr,threadRoutine,(void*)"thread 1");
while(true)
{
cout<<"main thread : "<<g_val<<" &: "<<&g_val <<endl;
sleep(1);
break;
}
// //如果我们要强行获取呢
int n=pthread_join(tid,nullptr);
cout<<"n: "<<n<<" errstring: "<<strerror(n)<<endl;
return 0;
}
非法的参数
我们将我们的上面的join注释掉就能够正常使用了。
如果主线程退出那么我们的进程也会退出我们所有的分离的线程也会被退出。
无论在多线程还是多进程的情况下我们都需要让我们的主线程/主进程最后退出。
什么情况下我们能分离
一般情况下是我们的主线程永远都是不退出的我们需要用到线程分离。
比方说我们的服务器我们的用户发起一个请求我们的主线程分离了一个线程给我们的用户
如果我们的线程被分离了那么我们的线程异常的话我们还会干扰别的线程吗
虽然我们的线程被分离了但是我们的资源还是我们的进程的资源也就是说我们的进程就会收到一个错误信号然后我们的一整个进程都会别退出。
#include<iostream>
#include<pthread.h>
#include<string>
#include<cstdio>
#include<cstring>
#include<unistd.h>
#include<errno.h>
using namespace std;
__thread int g_val=0;
void *threadRoutine(void *args)
{
pthread_detach(pthread_self());
sleep(5);
int i=0;
i/=0;
pthread_exit((void*)11);
}
int main()
{
pthread_t tid;
pthread_create(&tid ,nullptr,threadRoutine,(void*)"thread 1");
while(true)
{
cout<<"main thread : "<<g_val<<" &: "<<&g_val <<endl;
sleep(1);
// break;
}
return 0;
}
C++的线程库调用了我们系统的pthread线程库
#include<iostream>
// #include<pthread.h>
#include<string>
#include<cstdio>
#include<cstring>
#include<unistd.h>
#include<errno.h>
#include<thread>
using namespace std;
void fun()
{
while(true)
{
cout<<"hello new thread"<<endl;
sleep(1);
}
}
int main()
{
std::thread t(fun);
while(true)
{
cout<<"hello main thread"<<endl;
sleep(1);
}
t.join();
return 0;
}
如果我们再编译的时候不链接我们的pthread库
mythread:mythread.cc
g++ -o mythread mythread.cc -std=c++11
.PHONY:clean
clean:
rm -f mythread
我们语言的底层必须支持原生线程库不然就会产生下面的报错语言层面的库其实是对系统的库的封装。
只要我们依旧加上我们的pthread库就可以正常运行了
mythread:mythread.cc
g++ -o mythread mythread.cc -std=c++11 -lpthread
.PHONY:clean
clean:
rm -f mythread
六、线程互斥
进程线程间的互斥相关背景概念
临界资源多线程执行流共享的资源就叫做临界资源
临界区每个线程内部访问临界资源的代码就叫做临界区
互斥任何时刻互斥保证有且只有一个执行流进入临界区访问临界资源通常对临界资源起保护作用
原子性不会被任何调度机制打断的操作该操作只有两态要么完成要么未完成
互斥量mutex
大部分情况线程使用的数据都是局部变量变量的地址空间在线程栈空间内这种情况变量归属单个线程其他线程无法获得这种变量。
但有时候很多变量都需要在线程间共享这样的变量称为共享变量可以通过数据的共享完成线程之间的交互。
多个线程并发的操作共享变量会带来一些问题。
多线程抢票
#include<iostream>
#include<pthread.h>
#include<string>
#include<cstdio>
#include<cstring>
#include<unistd.h>
#include<errno.h>
#include<thread>
using namespace std;
//如果多线程访问一个全局变量并对它进行数据计算多线程会相互影响吗
//这里的tickets就是临界资源
int tickets=100;
void *getTickets(void* args)
{
(void)args;
while(true)
{
if(tickets>0)//1.判断的本质也是计算的一种
//把数据读取到寄存器本质上是将数据放入上下文数据当中
//
{
usleep(1000);
printf("%p: %d\n",pthread_self(),tickets);
tickets--;//2.也可能出现问题
}else{
break;
}
}
}
int main()
{
pthread_t t1,t2,t3;
//多线程抢票的逻辑
pthread_create(&t1,nullptr,getTickets,nullptr);
pthread_create(&t2,nullptr,getTickets,nullptr);
pthread_create(&t3,nullptr,getTickets,nullptr);
pthread_join(t1,nullptr);
pthread_join(t2,nullptr);
pthread_join(t3,nullptr);
}
这里我们观察到了两个100甚至还有0-1。
每个线程都有自己的时间片一个线程可以在自己的时间片内多次抢票。如果我们的线程切换越多我们的程序就可能会出问题。
如果我们的每一个线程都想要访问我们的共享区当中的tickets如果要–我们要完成3个动作。
1.我们将tickets读取到线程的上下文当中
2.进行–
3.写回我们的共享区中。
在这三个命令运行的期间都可能由于线程的调度导致我们并没有–我们的线程就被切换走了。当前的所有数据都会被保存给当前线程作为我们的线程的上下文但是我们的数据并没有写回。这期间别的线程进行–。当我们这个线程又重新运行的时候上下文的数据会被重新写回去也就会把别的线程的–给覆盖掉。
因为tickets被访问的时候没有被保护所以在并发访问的时候导致了我们的数据不一致的问题。
也就是说我们执行了上面的1-3部假如我们的线程1执行了1,2但是并没有执行3就被切换掉了然后我们的线程2开始执行我们的读取到的数据依旧是100。
如果我们的线程2并没有被打断并且成功地写回我们的tickets就会被–变成99。
假设我们的线程2一直–成功将tickets变成了50
执行完成之后如果我们的线程2被切走了我们的线程1被恢复了我们的操作系统需要将我们线程1的相关的上下文重新写回内存当中所以我们的tickets又变成了99这将会导致我们的数据出现错误。
加锁保护
互斥锁 全局
pthread_mutex_t
pthread_mutex_t就是原生线程库提供的一个数据类型。
pthread_mutex_lock
pthread_mutex_unlock
#include<iostream>
#include<pthread.h>
#include<string>
#include<cstdio>
#include<cstring>
#include<unistd.h>
#include<errno.h>
#include<thread>
using namespace std;
//如果多线程访问一个全局变量并对它进行数据计算多线程会相互影响吗
//这里的tickets就是临界资源
pthread_mutex_t mtx=PTHREAD_MUTEX_INITIALIZER;//初始化我们的锁
int tickets=100;
void *getTickets(void* args)
{
(void)args;
while(true)
{
//访问临界资源的代码区域临界区。
//对我们的临界区数据进行加锁
//任何一个时刻只允许一个线程拿到临界区资源别的线程进行等待直到这个线程释放掉这个锁的资源。
pthread_mutex_lock(&mtx);
if(tickets>0)
{
usleep(1000);
printf("%p: %d\n",pthread_self(),tickets);
tickets--;
pthread_mutex_unlock(&mtx);
}else{
pthread_mutex_unlock(&mtx);
break;
}
}
}
int main()
{
pthread_t t1,t2,t3;
//多线程抢票的逻辑
pthread_create(&t1,nullptr,getTickets,nullptr);
pthread_create(&t2,nullptr,getTickets,nullptr);
pthread_create(&t3,nullptr,getTickets,nullptr);
pthread_join(t1,nullptr);
pthread_join(t2,nullptr);
pthread_join(t3,nullptr);
}
这样我们就解决了我们的抢票问题。
但是我申请锁成功了别的资源就不能进行访问了那我们的程序的效率就会降低。
我们可以修改我们的代码让我们的线程的抢票更加明显
#include<iostream>
#include<pthread.h>
#include<string>
#include<cstdio>
#include<cstring>
#include<unistd.h>
#include<errno.h>
#include<time.h>
#include<thread>
using namespace std;
//如果多线程访问一个全局变量并对它进行数据计算多线程会相互影响吗
//这里的tickets就是临界资源
pthread_mutex_t mtx=PTHREAD_MUTEX_INITIALIZER;//初始化我们的锁
int tickets=100000;
void *getTickets(void* args)
{
(void)args;
while(true)
{
//访问临界资源的代码区域临界区。
//对我们的临界区数据进行加锁
//任何一个时刻只允许一个线程拿到临界区资源别的线程进行等待直到这个线程释放掉这个锁的资源。
//加锁的时候我们需要保证加锁的范围越小越好。
pthread_mutex_lock(&mtx);
if(tickets>0)
{
usleep(rand()%1500);
printf("%s: %d\n",args,tickets);
tickets--;
pthread_mutex_unlock(&mtx);
}else{
pthread_mutex_unlock(&mtx);
break;
}
usleep(rand()%200000);
}
}
int main()
{
srand((unsigned long)time(nullptr)^getpid()^0x147);
pthread_t t1,t2,t3;
//多线程抢票的逻辑
pthread_create(&t1,nullptr,getTickets,(void*)"thread 1");
pthread_create(&t2,nullptr,getTickets,(void*)"thread 2");
pthread_create(&t3,nullptr,getTickets,(void*)"thread 3");
pthread_join(t1,nullptr);
pthread_join(t2,nullptr);
pthread_join(t3,nullptr);
}
互斥锁局部
#include<iostream>
#include<pthread.h>
#include<string>
#include<cstdio>
#include<cstring>
#include<unistd.h>
#include<errno.h>
#include<time.h>
#include<thread>
#include<assert.h>
using namespace std;
//创建现成的数量
#define THREAD_NUM 100
class ThreadData
{
public:
ThreadData(const std::string &n,pthread_mutex_t *pm): tname(n),pmtx(pm){};
public:
std::string tname;
pthread_mutex_t *pmtx;
};
int tickets=10000;
void *getTickets(void* args)
{
ThreadData *td=(ThreadData*)args;
while(true)
{
int n=pthread_mutex_lock(td->pmtx);
assert(n==0);
if(tickets>0)
{
usleep(rand()%1500);
printf("%s: %d\n",td->tname.c_str(),tickets);
tickets--;
n=pthread_mutex_unlock(td->pmtx);
assert(n==0);
}else{
n=pthread_mutex_unlock(td->pmtx);
assert(n==0);
break;
}
usleep(rand()%2000);
}
delete td;
return nullptr;
}
int main()
{
time_t start=time(nullptr);
pthread_mutex_t mtx;
pthread_mutex_init(&mtx,nullptr);
srand((unsigned long)time(nullptr)^getpid()^0x147);
pthread_t t1,t2,t3;
pthread_t t[THREAD_NUM];
//多线程抢票的逻辑
for(int i=0;i<THREAD_NUM;i++)
{
std::string name="thread";
name+=std::to_string(i+1);
ThreadData *td=new ThreadData(name,&mtx);
pthread_create(t+i,nullptr,getTickets,(void*)td);
}
//阻塞式地等待所有的线程
for(int i=0;i<THREAD_NUM;i++)
{
pthread_join(t[i],nullptr);
}
time_t end=time(nullptr);
cout<<"cast: "<<(int)(end-start)<<endl;
}
加锁了之后线程在临界区中是否会切换会有问题吗
会切换
但是我们当前并没有释放锁。
所以即便是我们的别的线程抢占了CPU但是我们并没有释放锁所以其他的抢票线程想要执行临界区代码也必须先申请锁。
它申请锁是无法申请成功的。
所以我也不会让其他线程进入临界区就保证了临界区中数据。
我是一个线程我不申请锁就是单纯地访问临界资源那会有问题吗
这是一种错误的编码方式。
在没有持有锁的线程看来对我最有意义的情况只有两种
1线程1没有持有锁什么都没有做
2线程1释放锁做完此时我可以申请锁。
加锁了我们就是串行吗
对的。
是在执行临界区代码的时候一定是穿行的
要访问临界资源每一个线程必须先申请锁每一个线程都必须先看到同一把锁并且去访问它这把锁本身是不是就是一种共享资源呢那谁来保证锁的安全呢
所以为了保证锁的安全申请和释放锁必须是原子的。
如何保证申请是原子性的锁是如何实现的
如果我们再汇编的角度只有一条汇编语句我们就认为汇编语句的执行是原子的。
swap和exchange指令以一条汇编的方式将内存和CPU内寄存器的数据进行交换。
在执行流视角是如何看待我们CPU的寄存器的
CPU内部的寄存器本质叫做当前执行流的上下文寄存器们的空间是被所有的执行流共享的但是寄存器的内容是被每一个执行流私有的因为这是当前执行流的上下文。
申请锁和释放锁的原理
将0放入%al中
将寄存器的值和内存当中的mutex中的内容进行交换
return 0申请锁成功
假设我们的a被换上了处理机我们的%al中初始化为0我们的第一行代码
然后被切换成我们的线程B我们的线程A带着A的上下文离开我们的线程B带着上下文上处理机。
然后我们的线程B执行我们的第一行代码movb也就是将我们的B的%al初始化为0
然后线程B执行第二行代码将mtx和%al中的数据进行交换
这时我们发现我们的%al当中的内容为1大于0所以我们的线程B继续执行后面的代码然后就成功申请到了我们的锁。
假设我们此时A又被换上了处理机B带着B的上下文下处理机
在我们A上一次运行了第一行代码我们现在运行第二行代码也就是将%al和mutex进行交换。
但是交换了之后我们的%al还是0没办法进行后序的代码。在若干个时间片轮转之后只能是我们的线程B运行后序的代码。
交换的现象内存<–>%al做交换
交换的本质共享<–>私有
所以谁来保证锁的安全呢
由锁自己保证锁的安全性由一行汇编的原子性。
按照我们上面的故事我们的B重新被换上了处理机
然后我们线程B执行我们的unlock代码也就是
movb $1 ,mutex
也就是将mtx当中的数据设置为1
然后我们的B退出了我们的A重新换上处理机并将其上下文0换上%al然后执行我们上面lock代码当中的交换将%al和mtx的代码进行交换然后根据我们的lock的判断语句我们的A就可以运行了。
这里的1就是我们的锁。
可重入VS线程安全
线程安全多个线程并发同一段代码时不会出现不同的结果。常见对全局变量或者静态变量进行操作并且没有锁保护的情况下会出现该问题。
重入同一个函数被不同的执行流调用当前一个流程还没有执行完就有其他的执行流再次进入我们称之为重入。一个函数在重入的情况下运行结果不会出现任何不同或者任何问题则该函数被称为可重入函数否则是不可重入函数。
常见的线程不安全的情况
不保护共享变量的函数
函数状态随着被调用状态发生变化的函数
返回指向静态变量指针的函数
调用线程不安全函数的函数
常见的线程安全的情况
每个线程对全局变量或者静态变量只有读取的权限而没有写入的权限一般来说这些线程是安全的类或者接口对于线程来说都是原子操作
多个线程之间的切换不会导致该接口的执行结果存在二义性
常见可重入的情况不使用全局变量或静态变量
不使用用malloc或者new开辟出的空间
不调用不可重入函数
不返回静态或全局数据所有数据都有函数的调用者提供
使用本地数据或者通过制作全局数据的本地拷贝来保护全局数据
常见不可重入的情况
调用了malloc/free函数因为malloc函数是用全局链表来管理堆的
调用了标准I/O库函数标准I/O库的很多实现都以不可重入的方式使用全局数据结构
可重入函数体内使用了静态的数据结构
可重入与线程安全联系
函数是可重入的那就是线程安全的
函数是不可重入的那就不能由多个线程使用有可能引发线程安全问题
如果一个函数中有全局变量那么这个函数既不是线程安全也不是可重入的。
可重入与线程安全区别
可重入函数是线程安全函数的一种
线程安全不一定是可重入的而可重入函数则一定是线程安全的。
如果将对临界资源的访问加上锁则这个函数是线程安全的但如果这个重入函数若锁还未释放则会产生死锁因此是不可重入的。
线程死锁
我们申请的锁的数量可能不止一把锁。这里我们假设我们有两把锁。
我们的线程A需要申请 锁1 和 锁2并且是先申请 锁1然后申请 锁2
然后我们的线程B同样需要申请 锁1 和 锁2并且是先申请 锁2 然后申请 锁1
那么我们的线程A持有了锁1我们的线程B持有了锁2这两个线程都不释放各自的资源我们的线程都在互相申请对方的锁导致我们的两个线程都没办法向后运行这就是我们的线程死锁。
死锁四个必要条件
互斥条件一个资源每次只能被一个执行流使用
请求与保持条件一个执行流因请求资源而阻塞时对已获得的资源保持不放
不剥夺条件:一个执行流已获得的资源在末使用完之前不能强行剥夺
循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系
避免死锁
破坏死锁的四个必要条件
加锁顺序一致
避免锁未释放的场景
资源一次性分配
避免死锁算法
死锁检测算法
银行家算法
一把锁有可能形成死锁吗
有可能比方说我们的一个函数中申请了锁然后释放锁的代码写错了写成了申请锁那么我们的一把锁也可能会产生死锁。
七、线程同步
你在只有锁的情况可能会发生以下的情况
- 频繁地申请资源造成别人的饥饿的问题
- 太过于浪费自己和对方的资源。
所以说如果只有互斥的话就会导致太多的随机性。
比方说我们食堂抢饭就需要看运气谁能够抢到。
比方说抢票我们的所有人都在循环等待着那个买票的网站将票的信息发放出来这会导致我们的时间的浪费。而如果我们又在不断询问买票的工作人员我们又会耽误别人的时间
所以说我们的线程如果不知道什么时候有票就会不断询问什么时候有票。
所以我们需要引入同步的概念。
同步主要是为了解决访问临界资源合理性的问题的就是按照一定的顺序进行临界资源的访问也就是实现线程同步
方案1条件变量
当我们申请临界资源之前->先要做临界资源是否存在的检测的。
比方说我们的上面的买票需要先检测我们的票有没有卖完
要做检测的本质也是访问临界资源。你在检测的时候别人也在修改
结论对临界资源的检测也一定是需要在加锁和解锁之间的。
常规方式要检测条件就绪就注定了我们必须频繁申请和释放锁。
那有没有办法让我们的线程检测到我们资源不就绪的时候
1.不要让线程再自己频繁检测了
2.当条件就绪的时候我们可以通知对应的线程让他进行资源的申请和访问。
所以我们就需要引入条件变量
条件变量当一个线程互斥地访问某个变量时它可能发现在其它线程改变状态之前它什么也做不了。
例如一个线程访问队列时发现队列为空它只能等待只到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量。
同步概念与竞态条件同步在保证数据安全的前提下让线程能够按照某种特定的顺序访问临界资源从而有效避免饥饿问题叫做同步。
竞态条件因为时序问题而导致程序异常我们称之为竞态条件。在线程场景下这种问题也不难理解
pthread_cond_init
pthread_cond_wait
让我们的线程进行等待
![[Pasted image 20230113105409.png]]
int pthread_cond_wait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex);
//这里我们需要传入一把互斥锁
pthread_cond_signal
当我们的条件就绪的时候我们就需要唤醒我们的线程
![[Pasted image 20230113105607.png]]
所有的函数成功返回0错误返回错误码。
头文件都是pthread库
我们想要实现主线程控制其他线程等待和唤醒的效果
#include<iostream>
#include<pthread.h>
#include<string>
#include<thread>
#include<unistd.h>
using namespace std;
//创建现成的数量
#define TNUM 4
//定义一个函数指针
typedef void(*func_t)(const std::string &name);
class ThreadData{
public:
ThreadData(const std::string &name,func_t func):name_(name),func_(func){};
public:
std::string name_;
func_t func_;
};
//定义入口函数
void func1(const std::string &name)
{
while(true)
{
std::cout<<name<<"running -a"<<std::endl;
sleep(1);
}
}
//定义入口函数
void func2(const std::string &name)
{
while(true)
{
std::cout<<name<<"running -b"<<std::endl;
sleep(1);
}
}
//定义入口函数
void func3(const std::string &name)
{
while(true)
{
std::cout<<name<<"running -c"<<std::endl;
sleep(1);
}
}
//定义入口函数
void func4(const std::string &name)
{
while(true)
{
std::cout<<name<<"running -d"<<std::endl;
sleep(1);
}
}
//定义一个分发函数
void *Entry(void* args)
{
//这里的td是一个局部变量都是在每一个线程的独立的栈结构上进行调用的。
//所以不会产生线程的冲突。
ThreadData *td=(ThreadData*) args;
//它是一个函数调用完成就要返回。
td->func_(td->name_);
//将我们的资源进行释放。
delete td;
}
int main()
{
//创建一个线程数组将所有的现成的编号保存起来
pthread_t tids[TNUM];
//创建一个函数指针数组
func_t funcs[TNUM]={func1,func2,func3,func4};
for(int i=0;i<TNUM;i++)
{
std::string name="Thread";
name+=std::to_string(i+1);
ThreadData *td=new ThreadData(name,funcs[i]);
//线程编号线程属性我们默认设置nullptr
pthread_create(tids+i,nullptr,Entry,(void*)td);
}
//等待所有的线程退出
for(int i=0;i<TNUM;i++)
{
pthread_join(tids[i],nullptr);
std::cout<<"thread: "<<tids[i]<<"quit"<<std::endl;
}
}
控制对应的线程。
#include<iostream>
#include<pthread.h>
#include<string>
#include<thread>
#include<unistd.h>
using namespace std;
//创建现成的数量 #define TNUM 4
// pthread_cond_t cond PTHREAD_COND_INITIALIZER; // pthread_mutex_t mtx=PTHREAD_MUTEX_INITIALIZER; //定义一个函数指针 typedef void(*func_t)(const std::string &name,pthread_mutex_t *pmtx,pthread_cond_t * cond);
class ThreadData{
public:
ThreadData(const std::string &name,func_t func,pthread_mutex_t *pmtx,pthread_cond_t * cond)
:name_(name),
func_(func),
pmtx_(pmtx),
pcond_(cond)
{};
public:
std::string name_;
func_t func_;
pthread_mutex_t *pmtx_;
pthread_cond_t *pcond_;
};
void func1(const std::string &name,pthread_mutex_t *pmtx,pthread_cond_t * pcond)
{
while(true)
{
//传入条件变量和互斥锁
pthread_cond_wait(pcond,pmtx);//默认该线程再执行的时候wait代码被执行当前线程会立即被阻塞。
//将当前线程放入某些队列中进行等待。
std::cout<<name<<"running -a"<<std::endl;
}
}
void func2(const std::string &name,pthread_mutex_t *pmtx,pthread_cond_t * pcond)
{
while(true)
{
//传入条件变量和互斥锁
pthread_cond_wait(pcond,pmtx);//默认该线程再执行的时候wait代码被执行当前线程会立即被阻塞。
std::cout<<name<<"running -b"<<std::endl;
}
}
void func3(const std::string &name,pthread_mutex_t *pmtx,pthread_cond_t * pcond)
{
while(true)
{
//传入条件变量和互斥锁
pthread_cond_wait(pcond,pmtx);//默认该线程再执行的时候wait代码被执行当前线程会立即被阻塞。
std::cout<<name<<"running -c"<<std::endl;
}
}
void func4(const std::string &name,pthread_mutex_t *pmtx,pthread_cond_t * pcond)
{
while(true)
{
//传入条件变量和互斥锁
pthread_cond_wait(pcond,pmtx);//默认该线程再执行的时候wait代码被执行当前线程会立即被阻塞。
std::cout<<name<<"running -d"<<std::endl;
}
}
void *Entry(void* args)
{
//这里的td是一个局部变量都是在每一个线程的独立的栈结构上进行调用的。
//所以不会产生线程的冲突。
ThreadData *td=(ThreadData*) args;
//它是一个函数调用完成就要返回。
td->func_(td->name_,td->pmtx_,td->pcond_);
//将我们的资源进行释放。
delete td;
}
int main()
{
pthread_mutex_t mtx;
pthread_cond_t cond;
//对我们的锁和条件变量初始化
pthread_mutex_init(&mtx,nullptr);
pthread_cond_init(&cond,nullptr);
//创建一个线程数组将所有的现成的编号保存起来
pthread_t tids[TNUM];
//创建一个函数指针数组
func_t funcs[TNUM]={func1,func2,func3,func4};
for(int i=0;i<TNUM;i++)
{
std::string name="Thread";
name+=std::to_string(i+1);
ThreadData *td=new ThreadData(name,funcs[i],&mtx,&cond);
//线程编号线程属性我们默认设置nullptr
pthread_create(tids+i,nullptr,Entry,(void*)td);
}
sleep(5);
//主线程逐个唤醒我们的子线程
while(true)
{
std::cout<<"resume thread run code ...."<<std::endl;
//我们传入条件变量
//我们不关心唤醒的是哪一个线程有哪些线程我只关心将条件变量从无效设为有效的时候唤醒一个线程执行任务
// pthread_cond_signal(&cond);
pthread_cond_signal(&cond);
sleep(1);
}
//等待所有的线程退出
for(int i=0;i<TNUM;i++)
{
pthread_join(tids[i],nullptr);
std::cout<<"thread: "<<tids[i]<<"quit"<<std::endl;
}
pthread_mutex_destroy(&mtx);
pthread_cond_destroy(&cond);
}
为什么我们这里的线程的调度是有序的
因为我们是将我们的线程一个个逐个唤醒的先被唤醒的队列在执行完代码之后就会重新被放入我们的调度的队尾所以我们的线程这里的调度是有序的。
同时唤醒全部的线程
#include<iostream>
#include<pthread.h>
#include<string>
#include<thread>
#include<unistd.h>
using namespace std;
//创建现成的数量 #define TNUM 4
// pthread_cond_t cond PTHREAD_COND_INITIALIZER;
// pthread_mutex_t mtx=PTHREAD_MUTEX_INITIALIZER;
//定义一个函数指针
typedef void(*func_t)(const std::string &name,pthread_mutex_t *pmtx,pthread_cond_t * cond);
class ThreadData{
public:
ThreadData(const std::string &name,func_t func,pthread_mutex_t *pmtx,pthread_cond_t * cond)
:name_(name),
func_(func),
pmtx_(pmtx),
pcond_(cond)
{};
public:
std::string name_;
func_t func_;
pthread_mutex_t *pmtx_;
pthread_cond_t *pcond_;
};
void func1(const std::string &name,pthread_mutex_t *pmtx,pthread_cond_t * pcond)
{
while(true)
{
//传入条件变量和互斥锁
pthread_cond_wait(pcond,pmtx);//默认该线程再执行的时候wait代码被执行当前线程会立即被阻塞。
//将当前线程放入某些队列中进行等待。
std::cout<<name<<"running -a"<<std::endl;
}
}
void func2(const std::string &name,pthread_mutex_t *pmtx,pthread_cond_t * pcond)
{
while(true)
{
//传入条件变量和互斥锁
pthread_cond_wait(pcond,pmtx);//默认该线程再执行的时候wait代码被执行当前线程会立即被阻塞。
std::cout<<name<<"running -b"<<std::endl;
}
}
void func3(const std::string &name,pthread_mutex_t *pmtx,pthread_cond_t * pcond)
{
while(true)
{
//传入条件变量和互斥锁
pthread_cond_wait(pcond,pmtx);//默认该线程再执行的时候wait代码被执行当前线程会立即被阻塞。
std::cout<<name<<"running -c"<<std::endl;
}
}
void func4(const std::string &name,pthread_mutex_t *pmtx,pthread_cond_t * pcond)
{
while(true)
{
//传入条件变量和互斥锁
pthread_cond_wait(pcond,pmtx);//默认该线程再执行的时候wait代码被执行当前线程会立即被阻塞。
std::cout<<name<<"running -d"<<std::endl;
}
}
void *Entry(void* args)
{
//这里的td是一个局部变量都是在每一个线程的独立的栈结构上进行调用的。
//所以不会产生线程的冲突。
ThreadData *td=(ThreadData*) args;
//它是一个函数调用完成就要返回。
td->func_(td->name_,td->pmtx_,td->pcond_);
//将我们的资源进行释放。
delete td;
}
int main()
{
pthread_mutex_t mtx;
pthread_cond_t cond;
//对我们的锁和条件变量初始化
pthread_mutex_init(&mtx,nullptr);
pthread_cond_init(&cond,nullptr);
//创建一个线程数组将所有的现成的编号保存起来
pthread_t tids[TNUM];
//创建一个函数指针数组
func_t funcs[TNUM]={func1,func2,func3,func4};
for(int i=0;i<TNUM;i++)
{
std::string name="Thread";
name+=std::to_string(i+1);
ThreadData *td=new ThreadData(name,funcs[i],&mtx,&cond);
//线程编号线程属性我们默认设置nullptr
pthread_create(tids+i,nullptr,Entry,(void*)td);
}
sleep(5);
//主线程逐个唤醒我们的子线程
while(true)
{
std::cout<<"resume thread run code ...."<<std::endl;
//我们传入条件变量
//我们不关心唤醒的是哪一个线程有哪些线程我只关心将条件变量从无效设为有效的时候唤醒一个线程执行任务
// pthread_cond_signal(&cond);
pthread_cond_broadcast(&cond);
sleep(1);
}
//等待所有的线程退出
for(int i=0;i<TNUM;i++)
{
pthread_join(tids[i],nullptr);
std::cout<<"thread: "<<tids[i]<<"quit"<<std::endl;
}
pthread_mutex_destroy(&mtx);
pthread_cond_destroy(&cond);
}
这里我们想让我们的线程被唤醒10次然后我们的主线程退出
这里我们设计了一个quit标记位。
#include<iostream>
#include<pthread.h>
#include<string>
#include<thread>
#include<unistd.h>
using namespace std;
//创建现成的数量
#define TNUM 4
// pthread_cond_t cond PTHREAD_COND_INITIALIZER;
// pthread_mutex_t mtx=PTHREAD_MUTEX_INITIALIZER;
//定义一个函数指针
typedef void(*func_t)(const std::string &name,pthread_mutex_t *pmtx,pthread_cond_t * cond);
//默认设置全局数据不退出
volatile bool quit =false;
class ThreadData{
public:
ThreadData(const std::string &name,func_t func,pthread_mutex_t *pmtx,pthread_cond_t * cond)
:name_(name),
func_(func),
pmtx_(pmtx),
pcond_(cond)
{};
public:
std::string name_;
func_t func_;
pthread_mutex_t *pmtx_;
pthread_cond_t *pcond_;
};
void func1(const std::string &name,pthread_mutex_t *pmtx,pthread_cond_t * pcond)
{
while(!quit)
{
//传入条件变量和互斥锁
//检测资源不就绪的时候我们才会等
//但是检测资源不就绪的操作是在临界资源当中检测的。
//这里的wait一定要在加锁和解锁之间进行wait
//因为我们的wait是要访问临界区资源的
pthread_cond_wait(pcond,pmtx);//默认该线程再执行的时候wait代码被执行当前线程会立即被阻塞。
//将当前线程放入某些队列中进行等待。
std::cout<<name<<"running -a"<<std::endl;
}
}
void func2(const std::string &name,pthread_mutex_t *pmtx,pthread_cond_t * pcond)
{
while(!quit)
{
//传入条件变量和互斥锁
pthread_cond_wait(pcond,pmtx);//默认该线程再执行的时候wait代码被执行当前线程会立即被阻塞。
std::cout<<name<<"running -b"<<std::endl;
}
}
void func3(const std::string &name,pthread_mutex_t *pmtx,pthread_cond_t * pcond)
{
while(!quit)
{
//传入条件变量和互斥锁
pthread_cond_wait(pcond,pmtx);//默认该线程再执行的时候wait代码被执行当前线程会立即被阻塞。
std::cout<<name<<"running -c"<<std::endl;
}
}
void func4(const std::string &name,pthread_mutex_t *pmtx,pthread_cond_t * pcond)
{
while(!quit)
{
//传入条件变量和互斥锁
pthread_cond_wait(pcond,pmtx);//默认该线程再执行的时候wait代码被执行当前线程会立即被阻塞。
std::cout<<name<<"running -d"<<std::endl;
}
}
void *Entry(void* args)
{
//这里的td是一个局部变量都是在每一个线程的独立的栈结构上进行调用的。
//所以不会产生线程的冲突。
ThreadData *td=(ThreadData*) args;
//它是一个函数调用完成就要返回。
td->func_(td->name_,td->pmtx_,td->pcond_);
//将我们的资源进行释放。
delete td;
}
int main()
{
pthread_mutex_t mtx;
pthread_cond_t cond;
//对我们的锁和条件变量初始化
pthread_mutex_init(&mtx,nullptr);
pthread_cond_init(&cond,nullptr);
//创建一个线程数组将所有的现成的编号保存起来
pthread_t tids[TNUM];
//创建一个函数指针数组
func_t funcs[TNUM]={func1,func2,func3,func4};
for(int i=0;i<TNUM;i++)
{
std::string name="Thread";
name+=std::to_string(i+1);
ThreadData *td=new ThreadData(name,funcs[i],&mtx,&cond);
//线程编号线程属性我们默认设置nullptr
pthread_create(tids+i,nullptr,Entry,(void*)td);
}
sleep(5);
int cnt=10;
//主线程逐个唤醒我们的子线程
while(cnt)
{
std::cout<<"resume thread run code ...."<<cnt--<<std::endl;
//我们传入条件变量
//我们不关心唤醒的是哪一个线程有哪些线程我只关心将条件变量从无效设为有效的时候唤醒一个线程执行任务
// pthread_cond_signal(&cond);
pthread_cond_broadcast(&cond);
sleep(1);
}
//想让线程退出了
quit=true;
//等待所有的线程退出
for(int i=0;i<TNUM;i++)
{
pthread_join(tids[i],nullptr);
std::cout<<"thread: "<<tids[i]<<"quit"<<std::endl;
}
pthread_mutex_destroy(&mtx);
pthread_cond_destroy(&cond);
}
为什么没有正常退出我们的主线程
我们这里想让我们的线程在进行运行的时候在收到了对应的条件变量的时候再进行唤醒而不是一直轮询地去询问我们的资源是否已经就绪。
所以我们其实需要在我们的进程判断是否需要等待如果我们的条件变量不满足条件的时候也就是我们的资源不就绪我们就需要进行等待等别的线程将其唤醒。
我们这里的线程去判断是否需要等待的时候也就是需要去访问条件变量但是我们这里的条件变量是一个临界资源需要加上互斥锁进行保护。不然我们上一次访问我们的条件变量还满足的时候我们下一行代码还没执行我们的条件变量就不满足了但是我们当前的线程不知道这就会导致错误。
#include<iostream>
#include<pthread.h>
#include<string>
#include<thread>
#include<unistd.h>
using namespace std;
//创建现成的数量
#define TNUM 4
// pthread_cond_t cond PTHREAD_COND_INITIALIZER;
// pthread_mutex_t mtx=PTHREAD_MUTEX_INITIALIZER;
//定义一个函数指针
typedef void(*func_t)(const std::string &name,pthread_mutex_t *pmtx,pthread_cond_t * cond);
//默认设置全局数据不退出
volatile bool quit =false;
class ThreadData{
public:
ThreadData(const std::string &name,func_t func,pthread_mutex_t *pmtx,pthread_cond_t * cond)
:name_(name),
func_(func),
pmtx_(pmtx),
pcond_(cond)
{};
public:
std::string name_;
func_t func_;
pthread_mutex_t *pmtx_;
pthread_cond_t *pcond_;
};
void func1(const std::string &name,pthread_mutex_t *pmtx,pthread_cond_t * pcond)
{
while(!quit)
{
//传入条件变量和互斥锁
//检测资源不就绪的时候我们才会等
//但是检测资源不就绪的操作是在临界资源当中检测的。
//这里的wait一定要在加锁和解锁之间进行wait
//因为我们的wait是要访问临界区资源的
//加锁
pthread_mutex_lock(pmtx);
// if(临界资源是否就绪)如果是否的话就直接释放让别的线程继续
// 但是这样频繁释放锁不好我们需要让线程去等等待别的线程释放了之后再去唤醒。
pthread_cond_wait(pcond,pmtx);//默认该线程再执行的时候wait代码被执行当前线程会立即被阻塞。
//将当前线程放入某些队列中进行等待。
std::cout<<name<<"running -a"<<std::endl;
pthread_mutex_unlock(pmtx);
}
}
void func2(const std::string &name,pthread_mutex_t *pmtx,pthread_cond_t * pcond)
{
while(!quit)
{
pthread_mutex_lock(pmtx);
//传入条件变量和互斥锁
pthread_cond_wait(pcond,pmtx);//默认该线程再执行的时候wait代码被执行当前线程会立即被阻塞。
std::cout<<name<<"running -b"<<std::endl;
pthread_mutex_unlock(pmtx);
}
}
void func3(const std::string &name,pthread_mutex_t *pmtx,pthread_cond_t * pcond)
{
while(!quit)
{
pthread_mutex_lock(pmtx);
//传入条件变量和互斥锁
pthread_cond_wait(pcond,pmtx);//默认该线程再执行的时候wait代码被执行当前线程会立即被阻塞。
std::cout<<name<<"running -c"<<std::endl;
pthread_mutex_unlock(pmtx);
}
}
void func4(const std::string &name,pthread_mutex_t *pmtx,pthread_cond_t * pcond)
{
while(!quit)
{
pthread_mutex_lock(pmtx);
//传入条件变量和互斥锁
pthread_cond_wait(pcond,pmtx);//默认该线程再执行的时候wait代码被执行当前线程会立即被阻塞。
std::cout<<name<<"running -d"<<std::endl;
pthread_mutex_unlock(pmtx);
}
}
void *Entry(void* args)
{
//这里的td是一个局部变量都是在每一个线程的独立的栈结构上进行调用的。
//所以不会产生线程的冲突。
ThreadData *td=(ThreadData*) args;
//它是一个函数调用完成就要返回。
td->func_(td->name_,td->pmtx_,td->pcond_);
//将我们的资源进行释放。
delete td;
}
int main()
{
pthread_mutex_t mtx;
pthread_cond_t cond;
//对我们的锁和条件变量初始化
pthread_mutex_init(&mtx,nullptr);
pthread_cond_init(&cond,nullptr);
//创建一个线程数组将所有的现成的编号保存起来
pthread_t tids[TNUM];
//创建一个函数指针数组
func_t funcs[TNUM]={func1,func2,func3,func4};
for(int i=0;i<TNUM;i++)
{
std::string name="Thread";
name+=std::to_string(i+1);
ThreadData *td=new ThreadData(name,funcs[i],&mtx,&cond);
//线程编号线程属性我们默认设置nullptr
pthread_create(tids+i,nullptr,Entry,(void*)td);
}
sleep(5);
int cnt=10;
//主线程逐个唤醒我们的子线程
while(cnt)
{
std::cout<<"resume thread run code ...."<<cnt--<<std::endl;
//我们传入条件变量
//我们不关心唤醒的是哪一个线程有哪些线程我只关心将条件变量从无效设为有效的时候唤醒一个线程执行任务
// pthread_cond_signal(&cond);
pthread_cond_broadcast(&cond);
sleep(1);
}
//想让线程退出了
std::cout<<"ctrl done"<<std::endl;
quit=true;
pthread_cond_broadcast(&cond);
//等待所有的线程退出
for(int i=0;i<TNUM;i++)
{
pthread_join(tids[i],nullptr);
std::cout<<"thread: "<<tids[i]<<"quit"<<std::endl;
}
pthread_mutex_destroy(&mtx);
pthread_cond_destroy(&cond);
}
- 条件满足的时候我们再唤醒特定的线程–我们怎么知道条件是否满足呢
- mutex的意义。
生产消费模型
我们生活当中每一个人都有不同的消费需求。然后我们想要消费我们可以去超市买东西。
超市并不生产产品超市中的产品都是从供货商来的。
这里我们就是消费者供货商就是生产者超市就是交易场所。
每一个商品都有对应的供货商提供给我们的。
![[Pasted image 20230113122319.png]]
我们是不能直接去供货商哪里买东西的因为供货商是批量生产的但是我们可能仅仅是买一点点。
我们发现供货商只关心他的商场我们的超市只关心如何将商品消费给我们的消费者。
这样我们就实现了逻辑上的解耦从而提高了我们的效率。
所以我们超市的本质就是商品的缓冲区也就是将我们生产者的商品暂时缓冲到我们的超市中等待我们的消费者购买对应的商品。
生产者之间属于竞争关系因为一家的商品放到我们的货架上之后别的供货商就没办法放入对应的商品了。
3种关系生产者和生产者生产者和消费者消费者和消费者
竞争互斥关系
互斥
供货商供货的时候你不能去拿比方说想要打印一个长字符串你取走了一部分这样就不对了
同步关系当没货的时候我们的生产者就需要进货我们的货有了之后我们的消费者需要进行消费。
竞争互斥关系抢显卡
2种角色生产者、消费者
1个交易场所超市
超市是被所有的生产者所共享的超市同样也是被我们的消费者共享的。
假设我们的生产者向我们的商场中提供的是同一种商品
站在工程师的角度
生产者和消费者都是由我们的线程扮演的角色。
这里的超市是某种数据结构表示的缓冲区。
这里商品就是数据。
也就是一部分的线程向我们的缓冲区中传入数据一部分线程向我们的缓冲区中取走数据。
当我们的生产者生产了商品我们的生产者就可以通知消费者进行消费了
我们的消费者进行消费了我们的消费者就可以通知生产生继续进行生产了。
也就是生产者线程往缓冲区当中传入了数据就通知消费者线程取走数据
我们的消费者线程取走了数据就通知生产者线程生产数据。