C++【多线程】
创始人
2024-05-10 20:37:35
0

文章目录

  • 一、什么是线程
  • 二、创建线程
    • pthread_create
    • pthread_join
  • 三、线程退出
    • pthread_exit
    • pthread_cancel
    • 线程id
    • pthread_self
  • 四、进程对于共享资源的访问
    • __thread
  • 五、分离线程
    • pthread_detach
  • 六、线程互斥
    • 加锁保护
      • 互斥锁 (全局)
    • pthread_mutex_t
    • pthread_mutex_lock
    • pthread_mutex_unlock
      • 互斥锁(局部)
    • 申请锁和释放锁的原理
    • 线程死锁

一、什么是线程

线程在进程内部执行,是OS调度的基本单位。

linux进程

在堆区上存在下面一种数据结构

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的基本单位是4kb,IO就是将页帧装进页框里

在这里插入图片描述

缺页中断:如果我们的操作系统在寻值得时候,发现对应的数据不在我们的内存中,我们就需要去磁盘中读取对应的数据到我们的内存中,然后通过页表映射,获取到我们的数据。

我们的虚拟地址有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操作。

线程的缺点

  1. 性能损失
    一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
  2. 健壮性降低
    编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
  3. 缺乏访问控制
    进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
  4. 编程难度提高
    编写与调试一个多线程程序比单线程程序困难得多

线程是不是越多越好?
线程越多,线程之间的切换也回更加频繁,这会导致系统的开销变大,导致我们的效率反而下降。一般我们创建线程的数量等于CPU的核心数。

二、创建线程

pthread_create

在这里插入图片描述
在这里插入图片描述

我们还需要在我们的makefile中添加-lpthead选项

mythread:mythread.ccg++ -o mythread mythread.cc -std=c++11  -lpthread
.PHONY:clean
clean:rm -f mythread
#include
#include
#include
#include
#include
using namespace std;
void *threadRun(void *args)
{const string name=(char*)args;while(true){//如果线程属于进程的话,我们这里获得的pid应该和我们的线程是同一个pidcout<//无符号长整数类型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: "<
我们这里看我们的程序已经链接上了我们的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内是有硬件级别的缓存的(cache)(L1-L3)
我们只要将相关的数据load到我们CPU内部的缓存,对内存的代码和数据,根据局部性原理
(一条指令如果被使用了,它附近的代码也有很大的可能被使用),预读取到我们的CPU的缓存中,
这样我们的CPU就不需要访问内存,直接到缓存中访问就可以了。但是如果进程切换,那么我们的cache立即失效,新进程过来的时侯,只能重新缓存。
所以我们的线程切换比我们的进程切换更加轻量化。
#include
#include
#include
#include
#include
using namespace std;
void *threadRoutine(void *args)
{while(true){cout<<"新线程: "<<(char*)args<<"running...."<pthread_t tid;pthread_create(&tid ,nullptr,threadRoutine,(void*)"thread 1");while(true){cout<<"main线程: "<<"running...."<

在这里插入图片描述

如果我们让新线程异常,我们的一整个进程都会因为这个异常而退出

#include
#include
#include
#include
#include
using namespace std;
void *threadRoutine(void *args)
{while(true){cout<<"新线程: "<<(char*)args<<"running...."<pthread_t tid;pthread_create(&tid ,nullptr,threadRoutine,(void*)"thread 1");while(true){cout<<"main线程: "<<"running...."<

在这里插入图片描述

线程在创建并执行的时候,线程也是需要进行等待的。
如果主线程不等待,就回引起类似于进程的僵尸问题,导致内存泄漏。

pthread_join

pthread_join是用来进行线程等待的。

在这里插入图片描述

#include
#include
#include
#include
#include
using namespace std;
void *threadRoutine(void *args)
{int i=0;while(true){cout<<"新线程: "<<(char*)args<<"running...."<break;}}
}
int main()
{pthread_t tid;pthread_create(&tid ,nullptr,threadRoutine,(void*)"thread 1");pthread_join(tid,nullptr);//默认会阻塞等待新线程的退出。cout<<"main thread wait done"<cout<<"main线程: "<<"running...."<

在这里插入图片描述

新线程的返回值返回给谁呢?

一般是给主线程,main_thread,main如何获取到呢?

 int pthread_join(pthread_t thread, void **retval);
#include
#include
#include
#include
#include
using namespace std;
void *threadRoutine(void *args)
{int i=0;while(true){cout<<"新线程: "<<(char*)args<<"running...."<break;}}cout<<"new thread quit ..."<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<cout<<"main线程: "<<"running...."<

在这里插入图片描述

多线程可以再新线程和主线程之间传递信息

这里我们可以传递一整个数组或者别的数据

#include
#include
#include
#include
#include
using namespace std;
void *threadRoutine(void *args)
{int i=0;int *data =new int[10];while(true){data[i]=i;cout<<"新线程: "<<(char*)args<<"running...."<break;}}cout<<"new thread quit ..."<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"<cout<

在这里插入图片描述

我们的主进程为什么没有获取新线程的退出码之类的接口?

一个线程崩了,整一个进程就崩掉了,获取退出码没有意义。
1.线程谁先运行与调度器有关
2.线程一旦异常,都可能导致整个进程整体退出
3.现成的输入和返回值问题
4.线程异常退出的理解

三、线程退出

pthread_exit

在多线程中不要调用exit

exit是用来终止进程的。

#include
#include
#include
#include
#include
using namespace std;
void *threadRoutine(void *args)
{int i=0;int *data =new int[10];while(true){data[i]=i;cout<<"新线程: "<<(char*)args<<"running...."<break;}}exit(10);cout<<"new thread quit ..."<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"<

这样调用的话,我们的 main thread wait done … main quit … :new thread quit"这句话就没办法打印出来,也就是说在quit之后,我们一整个进程就全部退出了!别的线程后面的代码根本就没办法执行。

在这里插入图片描述

所以我们使用pthread_exit()
#include
#include
#include
#include
#include
using namespace std;
void *threadRoutine(void *args)
{int i=0;int *data =new int[10];while(true){data[i]=i;cout<<"新线程: "<<(char*)args<<"running...."<break;}}pthread_exit((void*)10);cout<<"new thread quit ..."<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<

在这里插入图片描述

pthread_cancel

#include
#include
#include
#include
#include
using namespace std;
void *threadRoutine(void *args)
{int i=0;while(true){cout<<"新线程: "<<(char*)args<<"running...."<pthread_t tid;pthread_create(&tid ,nullptr,threadRoutine,(void*)"thread 1");int count=0;while(true){cout<<"main线程: "<<"running...."<=5) break;}pthread_cancel(tid);cout<<"pthread cancel: "<

在这里插入图片描述

1.线程被取消,join的时候,退出码是-1, #define PTHREAD_CANCELED((void*)-1)

主线程可以取消新线程,新线程可不可以处理掉主线程?

一般不这么做,一般都是用主线程去等待新线程的。不然我们的主线程的相关的信息没线程处理了。

线程id

#include
#include
#include
#include
#include
using namespace std;
void *threadRoutine(void *args)
{int i=0;while(true){cout<<"新线程: "<<(char*)args<<"running...."<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...."<=5) break;}pthread_cancel(tid);cout<<"pthread cancel: "<

在这里插入图片描述

这两个非常大的整数就是我们的线程id
这个tid是我们的线程id,本质上是一个地址!
因为我们目前用的不是Linux自带的常见线程的借口(不是操作系统的接口)
而是pthread库中的接口。
所以用户和操作系统之间就有一个软件层pthread库。
这里的线程也需要管理起来,操作系统需要负责调度方面,这个库需要提供用户方面的借口,和相关的字段,对应的栈结构。

想要在用户层对我们的线程进行管理的话,我们就需要再thread库中进行管理

多线程如何保证每一个线程独占栈区呢?
在用户层尽量会给提供
这个线程在我们的库中的相关属性的起始地址就是tid.
所以线程id就是一个地址。
我们主线程用的是内核级栈结构,我们的新线程用的就是库中的私有栈结构(用共享区地址来充当栈结构)。
在这里插入图片描述
在这里插入图片描述

pthread_self

获取自己的线程id

#include
#include
#include
#include
#include
using namespace std;
void *threadRoutine(void *args)
{int i=0;while(true){cout<<"新线程: "<<(char*)args<<"running...."<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:"<=5) break;}pthread_cancel(tid);cout<<"pthread cancel: "<

在这里插入图片描述

那我们是不是线程可以自己取消自己?

pthread_cancel(pthread_self())
可以,但是尽量不要这么做。

四、进程对于共享资源的访问

全局变量被多个线程共享

#include
#include
#include
#include
#include
using namespace std;
int g_val=0;
void *threadRoutine(void *args)
{while(true){cout<<(char*)args<<" : "<pthread_t tid;pthread_create(&tid ,nullptr,threadRoutine,(void*)"thread 1");while(true){cout<<"main thread : "<

在这里插入图片描述

__thread

__thread修饰全局变量,带来的结构就是让每一个线程各自拥有一个全局变量–现成的局部存储
也就是你每创建一个线程,这个全局变量就会给你拷贝几份。

#include
#include
#include
#include
#include
using namespace std;
__thread int g_val=0;
void *threadRoutine(void *args)
{while(true){cout<<(char*)args<<" : "<pthread_t tid;pthread_create(&tid ,nullptr,threadRoutine,(void*)"thread 1");while(true){cout<<"main thread : "<

在这里插入图片描述

如果我们再线程内部进行程序替换会发生什么?
会将一整个进程的代码和数据全部都替换掉,别的线程的代码也会被替换掉。

这里我们在新线程当中进行了进程替换,但是我们的主线程也被替换掉了。

#include
#include
#include
#include
#include
using namespace std;
__thread int g_val=0;
void *threadRoutine(void *args)
{sleep(5);execl("/bin/ls","ls",nullptr);while(true){cout<<(char*)args<<" : "<pthread_t tid;pthread_create(&tid ,nullptr,threadRoutine,(void*)"thread 1");while(true){cout<<"main thread : "<

也就是除了主线程之外,别的线程都不运行了,我们主线程的接下来的代码被替换成了ls指令。

在这里插入图片描述

五、分离线程

如果我不关心我线程的返回值,我只是想创建一个线程。这个线程在退出了之后,自动释放线程资源。(我们的主线程不需要去join了,新线程的资源会被库进行回收)

pthread_detach

分离线程,一般是线程自己分离自己

分离之后,就不能使用join了,如果我们想要强制使用join呢?

#include
#include
#include
#include
#include
#include
#include
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<<" : "<pthread_t tid;pthread_create(&tid ,nullptr,threadRoutine,(void*)"thread 1");while(true){cout<<"main thread : "<

非法的参数
在这里插入图片描述

我们将我们的上面的join注释掉就能够正常使用了。

如果主线程退出,那么我们的进程也会退出,我们所有的分离的线程也会被退出。
无论在多线程还是多进程的情况下,我们都需要让我们的主线程/主进程最后退出。

什么情况下我们能分离?

一般情况下是我们的主线程永远都是不退出的,我们需要用到线程分离。
比方说我们的服务器,我们的用户发起一个请求,我们的主线程分离了一个线程给我们的用户

如果我们的线程被分离了,那么我们的线程异常的话,我们还会干扰别的线程吗?

虽然我们的线程被分离了,但是我们的资源还是我们的进程的资源,也就是说我们的进程就会收到一个错误信号,然后我们的一整个进程都会别退出。

#include
#include
#include
#include
#include
#include
#include
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 : "<

在这里插入图片描述

C++的线程库调用了我们系统的pthread线程库

#include
// #include
#include
#include
#include
#include
#include
#include
using namespace std;void fun()
{while(true){cout<<"hello new thread"<std::thread t(fun);while(true){cout<<"hello main thread"<

在这里插入图片描述
如果我们再编译的时候,不链接我们的pthread库

mythread:mythread.ccg++ -o mythread mythread.cc -std=c++11
.PHONY:clean
clean:rm -f mythread

我们语言的底层必须支持原生线程库,不然就会产生下面的报错,语言层面的库其实是对系统的库的封装。
在这里插入图片描述
只要我们依旧加上我们的pthread库,就可以正常运行了

mythread:mythread.ccg++ -o mythread mythread.cc -std=c++11  -lpthread
.PHONY:clean
clean:rm -f mythread

六、线程互斥

进程线程间的互斥相关背景概念
临界资源:多线程执行流共享的资源就叫做临界资源
临界区:每个线程内部,访问临界资源的代码,就叫做临界区
互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成

互斥量mutex
大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。
但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。
多个线程并发的操作共享变量,会带来一些问题。

多线程抢票

#include
#include
#include
#include
#include
#include
#include
#include
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

![[Pasted image 20230112095735.png]]

#include  
#include  
#include  
#include  
#include  
#include  
#include  
#include  
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);  
}

这样我们就解决了我们的抢票问题。

![[Pasted image 20230112100120.png]]

但是我申请锁成功了,别的资源就不能进行访问了,那我们的程序的效率就会降低。

我们可以修改我们的代码,让我们的线程的抢票更加明显

#include  
#include  
#include  
#include  
#include  
#include  
#include  
#include  
#include  
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);  
}

![[Pasted image 20230112101430.png]]

互斥锁(局部)

#include  
#include  
#include  
#include  
#include  
#include  
#include  
#include  
#include  
#include  
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  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  pthread_join(t[i],nullptr);  }  time_t end=time(nullptr);  cout<<"cast: "<<(int)(end-start)<

![[Pasted image 20230112104502.png]]

加锁了之后,线程在临界区中是否会切换,会有问题吗

会切换!
但是我们当前并没有释放锁。
所以即便是我们的别的线程抢占了CPU,但是我们并没有释放锁,所以其他的抢票线程想要执行临界区代码,也必须先申请锁
它申请锁是无法申请成功的。
所以我也不会让其他线程进入临界区,就保证了临界区中数据。

![[Pasted image 20230112111942.png]]

我是一个线程,我不申请锁,就是单纯地访问临界资源,那会有问题吗?

这是一种错误的编码方式。
在没有持有锁的线程看来,对我最有意义的情况只有两种
1:线程1没有持有锁(什么都没有做)
2:线程1释放锁(做完),此时我可以申请锁。

加锁了我们就是串行吗?

对的。
是在执行临界区代码的时候一定是穿行的

要访问临界资源,每一个线程必须先申请锁,每一个线程都必须先看到同一把锁,并且去访问它,这把锁本身是不是就是一种共享资源呢?那谁来保证锁的安全呢?

所以为了保证锁的安全,申请和释放锁必须是原子的。

如何保证申请是原子性的?锁是如何实现的?

如果我们再汇编的角度,只有一条汇编语句,我们就认为汇编语句的执行是原子的。
swap和exchange指令:以一条汇编的方式,将内存和CPU内寄存器的数据进行交换。

在执行流视角,是如何看待我们CPU的寄存器的?

CPU内部的寄存器本质叫做当前执行流的上下文,寄存器们的空间是被所有的执行流共享的,但是寄存器的内容,是被每一个执行流私有的,因为这是当前执行流的上下文。

申请锁和释放锁的原理

![[Pasted image 20230112113050.png]]

将0放入%al中

>![[Pasted image 20230112113234.png]]

将寄存器的值和内存当中的mutex中的内容进行交换

![[Pasted image 20230112113318.png]]

return 0,申请锁成功

假设我们的a被换上了处理机,我们的%al中初始化为0(我们的第一行代码)

![[Pasted image 20230112114251.png]]

然后被切换成我们的线程B,我们的线程A带着A的上下文离开,我们的线程B带着上下文上处理机。
然后我们的线程B执行我们的第一行代码movb,也就是将我们的B的%al初始化为0

![[Pasted image 20230112114426.png]]

然后线程B执行第二行代码,将mtx和%al中的数据进行交换

![[Pasted image 20230112114633.png]]

这时我们发现我们的%al当中的内容为1,大于0,所以我们的线程B继续执行后面的代码,然后就成功申请到了我们的锁。

假设我们此时A又被换上了处理机,B带着B的上下文下处理机

![[Pasted image 20230112115352.png]]

在我们A上一次运行了第一行代码,我们现在运行第二行代码,也就是将%al和mutex进行交换。
但是交换了之后,我们的%al还是0,没办法进行后序的代码。在若干个时间片轮转之后,只能是我们的线程B运行后序的代码。

交换的现象:内存<–>%al做交换
交换的本质:共享<–>私有

所以谁来保证锁的安全呢?

由锁自己保证锁的安全性(由一行汇编的原子性)。

![[Pasted image 20230112115656.png]]

按照我们上面的故事,我们的B重新被换上了处理机

![[Pasted image 20230112115749.png]]

然后我们线程B执行我们的unlock代码,也就是
movb $1 ,mutex
也就是将mtx当中的数据设置为1

![[Pasted image 20230112120207.png]]

然后我们的B退出了,我们的A重新换上处理机,并将其上下文0换上%al,然后执行我们上面lock代码当中的交换,将%al和mtx的代码进行交换,然后根据我们的lock的判断语句,我们的A就可以运行了。

![[Pasted image 20230112120034.png]]

这里的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.dry(反义词)__________________  2.writ...
复活节英文怎么说 复活节英文怎么说?复活节的英语翻译是什么?复活节:Easter;"Easter,anniversar...
2008年北京奥运会主题曲 2008年北京奥运会(第29届夏季奥林匹克运动会),2008年8月8日到2008年8月24日在中华人...
英语道歉信 英语道歉信15篇  在日常生活中,道歉信的使用频率越来越高,通过道歉信,我们可以更好地解释事情发生的...
六年级英语专题训练(连词成句... 六年级英语专题训练(连词成句30题)  1. have,playhouse,many,I,toy,i...
上班迟到情况说明英语   每个人都或多或少的迟到过那么几次,因为各种原因,可能生病,可能因为交通堵车,可能是因为天气冷,有...
小学英语教学论文 小学英语教学论文范文  引导语:英语教育一直都是每个家长所器重的,那么有关小学英语教学论文要怎么写呢...
英语口语学习必看的方法技巧 英语口语学习必看的方法技巧如何才能说流利的英语? 说外语时,我们主要应做到四件事:理解、回答、提问、...
四级英语作文选:Birth ... 四级英语作文范文选:Birth controlSince the Chinese Governmen...
金融专业英语面试自我介绍 金融专业英语面试自我介绍3篇  金融专业的学生面试时,面试官要求用英语做自我介绍该怎么说。下面是小编...
我的李老师走了四年级英语日记... 我的李老师走了四年级英语日记带翻译  我上了五个学期的小学却换了六任老师,李老师是带我们班最长的语文...
小学三年级英语日记带翻译捡玉... 小学三年级英语日记带翻译捡玉米  今天,我和妈妈去外婆家,外婆家有刚剥的`玉米棒上带有玉米籽,好大的...
七年级英语优秀教学设计 七年级英语优秀教学设计  作为一位兢兢业业的人民教师,常常要写一份优秀的教学设计,教学设计是把教学原...
我的英语老师作文 我的英语老师作文(通用21篇)  在日常生活或是工作学习中,大家都有写作文的经历,对作文很是熟悉吧,...
英语老师教学经验总结 英语老师教学经验总结(通用19篇)  总结是指社会团体、企业单位和个人对某一阶段的学习、工作或其完成...
初一英语暑假作业答案 初一英语暑假作业答案  英语练习一(基础训练)第一题1.D2.H3.E4.F5.I6.A7.J8.C...
大学生的英语演讲稿 大学生的英语演讲稿范文(精选10篇)  使用正确的写作思路书写演讲稿会更加事半功倍。在现实社会中,越...
VOA美国之音英语学习网址 VOA美国之音英语学习推荐网址 美国之音网站已经成为语言学习最重要的资源站点,在互联网上还有若干网站...
商务英语期末试卷 Part I Term Translation (20%)Section A: Translate ...