当前位置: 首页 > news >正文

Linux 线程(中)

一、线程的局部存储

1.1 线程共享进程地址空间,在同一进程内线程的大部分资源都是共享的,但也有小部分资源是线程独立的

看一段代码:

#include <iostream>
#include<pthread.h>
#include<unistd.h>
using namespace std;
void* handler(void* args)
{int a=10;while(true){cout<<a++<<endl;printf("Thread stack addresss is : %p\n",&a);//打印a变量地址sleep(1);}return nullptr;
}
int main()
{pthread_t tid[3];for(int i=0;i<3;i++){pthread_create(tid+i,nullptr,handler,nullptr);}for(int i=0;i<3;i++){pthread_join(tid[i],nullptr);}return 0;
}

我们批量创建了三个线程,并对线程执行的函数中的变量进行了取地址,编译后运行:

打印结果可以看到,每一个线程的a的地址都不一样,且每个线程都是从10开始打印;说明线程调用的函数所创建出来的栈空间是属于每个线程自己的,也就是线程具有独立的栈空间;

1.2 --thread修饰后的全局变量

如果我们定义一个全局变量,让线程分别去访问并修改这个变量的内容,同时我们取一下这个全局变量的地址,看看结果如何:

以上代码稍作修改:

#include <iostream>
#include<pthread.h>
#include<unistd.h>
using namespace std;
int i=10;
void* handler(void* args)
{//int a=10;while(true){cout<<i++<<endl;printf("Thread stack addresss is : %p\n",&i);//打印a变量地址sleep(1);}return nullptr;
}
int main()
{pthread_t tid[3];for(int i=0;i<3;i++){pthread_create(tid+i,nullptr,handler,nullptr);}for(int i=0;i<3;i++){pthread_join(tid[i],nullptr);}return 0;
}

结果可以看到每个线程打印的i的地址都是一样的,且i是从10开始,每个线程都对齐进行++操作,也就是全局区对于每个线程来说都是共享的,能够共同见;

如果我们在变量定义前加--thread修饰后再运行

运行结果发现跟在线程的栈空间里创建变量一样,要每个线程的i的地址都不样,每个线程独有一份变量i的数据,且每个线程分别各自对其进行操作;

__thread:编译选项,修饰后的全局变量每个线程独有一份,__thread只能定义内置类型,不能修饰自定义类型;

二、多线程并发访问共享资源带来的问题

2.1、数据不一致

如果多个线程同时访问一个共享数据,当一个线程对其进行读取的同时另一个线程对其进行写入,这个时候就会发生数据不一致的问题:

代码稍作改动:

#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;int i = 10;
void *handler(void *args)
{// int a=10;while (true){if (i > 0){sleep(1);cout << i-- << endl;//sleep(1);printf("Thread stack addresss is : %p\n", &i); // 打印a变量地址}elsebreak;}return nullptr;
}
int main()
{pthread_t tid[3];for (int i = 0; i < 3; i++){pthread_create(tid + i, nullptr, handler, nullptr);}for (int i = 0; i < 3; i++){pthread_join(tid[i], nullptr);}return 0;
}

我们发现打印结果竟然打印出0、-1  ,我们明明写的判断条件是if(i>0),按道理i<=0的时候进入不到判断条件里面才对,后置--结果不会出现0、-1才对,这是为什么呢?

2.2、解决线程并发问题的方法:互斥

图解:

互斥概念:共享资源在任何时刻只能被一个执行流单独访问!

临界资源概念:任何时刻只能被一个执行流访问的资源为临界资源!

临界区概念:访问临界资源的代码!

解决并发问题的方法:我们让每个线程在执行if ~else 这段代码时保证在它执行的过程中不会出现第二个线程,等这个线程完成所有操作后其他线程才能进入if判断;

2.3 锁的概念

对应代码:

2.4 锁的接口
#include<pthread.h>
int pthread_mutex_init(pthread_mutex_t * restrict mutex, const pthread_mutexattr_t* restrict attr);
//1. 这是一个锁初始化的接口
//2. mutex: 需要初始化的锁的指针,attr:属性的设置一般设空;
#include<pthread.t>
int pthread_mutex_destroy(phtread_mutex_t* mutex);
//1. 这是一个销毁锁的接口,如果不是全局的锁就要初始化跟销毁(在程序结束前)
//2. mutex: 需要销毁的锁的指针
#include<pthread.h>
int pthread_mutex_lock(phread_mutex_t* mutex);
//1. 这是一个上锁的接口,线程调用这个接口时会阻塞等待获取锁直到获取为止;
//2. mutex: 你需要获取的锁的指针
#include<pthread.h>
int pthread_mutex_unlock(pthread_mutex_t* mutex);
//1. 这是一个释放锁的接口,上锁跟释放锁之间的代码为临界区,之间的资源为临界资源;
//2. mutex: 你需要释放的锁的指针
//3. 保证临界区代码越少越好
#include<pthread.h>
int pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALLIZER;
// 这是一个全局锁,如果设置了全局锁则不需要初始化跟销毁
2.5 锁的使用

有了锁的接口,那么锁加在哪里比较合适呢?

加锁的本质:用时间换区数据安全(因为每个线程进入之前都要检查锁,如果没持有锁就要先去获取锁,会处于一个阻塞等待的过程);

加锁的表现:线程对临界区代码的执行;

加锁的原则:尽量保证临界区代码越少越好;(如果代码很多,其他所有线程都要等待这一个正在访问临界资源的线程结束完才能进入,效率会很低)

代码:

全局锁:

#include <iostream>
#include <pthread.h>
#include <unistd.h>
pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;//全局锁
using namespace std;int i = 10;
void *handler(void *args)
{// int a=10;while (true){if(i>0){pthread_mutex_lock(&mutex);if (i > 0){sleep(1);cout << i-- << endl;//sleep(1);printf("Thread stack addresss is : %p\n", &i); // 打印a变量地址}pthread_mutex_unlock(&mutex);}elsebreak;}return nullptr;
}
int main()
{pthread_t tid[3];for (int i = 0; i < 3; i++){pthread_create(tid + i, nullptr, handler, nullptr);}for (int i = 0; i < 3; i++){pthread_join(tid[i], nullptr);}return 0;
}

如果不是全局锁的用法:

#include <iostream>
#include <pthread.h>
#include <unistd.h>
//pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;//全局锁
using namespace std;int i = 10;
void *handler(void *args)
{pthread_mutex_t*mutex=static_cast<pthread_mutex_t*>(args);//参数类型转换// int a=10;while (true){if(i>0){pthread_mutex_lock(mutex);//上锁if (i > 0){sleep(1);cout << i-- << endl;//sleep(1);printf("Thread stack addresss is : %p\n", &i); // 打印a变量地址}pthread_mutex_unlock(mutex);//解锁}elsebreak;}return nullptr;
}
int main()
{pthread_mutex_t mutex;//锁变量创建pthread_mutex_init(&mutex,nullptr);//锁初始化pthread_t tid[3];for (int i = 0; i < 3; i++){pthread_create(tid + i, nullptr, handler, &mutex);}for (int i = 0; i < 3; i++){pthread_join(tid[i], nullptr);}pthread_mutex_destroy(&mutex);//锁销毁return 0;
}

打印结果不再出现0跟-1;

三、互斥与同步

3.1、每个线程对锁的竞争能力都不同

假设某个线程对锁的竞争能力很强,就会导致每次都是这个线程获取到锁,每次都是他进入临界资源访问,其他线程只能在等待,就会造成线程饥饿的问题;

我们把i设为100,并在线程进入临界区后打印线程tid:

看到一直只有线程tid16进制为0x98c60700这一个线程在跑,其他两个线程进不来;

3.2、同步

同步的概念:在保证数据安全的前提下,让每个执行流按顺序的依次执行;

注意:这里保证数据安全的意思就是互斥,只有互斥数据才能安全,但是互斥会带了线程饥饿,解决线程饥饿就需要同步!

如何让每个执行流有顺序地去竞争资源呢?

图解:

这个每个线程去等待的地方我们称之为条件变量,条件变量里有一个等待队列,用来维护每一个在排队的线程,且让他们处于休眠状态,等待被唤醒,一旦被唤醒就把这个线程从队列里面去除(默认顺序是从第一个排队的线程开始)

3.3条件变量接口
#include<pthread.h>
int pthread_cond_init(pthread_cond_t* cond,const pthread_condattr_t* attr);
//1. 这是一个条件变量初始化的接口
//2. cond : 需要初始化的条件变量的指针, attr:条件变量的设置一般设为空
#incldue<pthread.h>
int pthread_cond_destroy(pthread_cond_t* cond);
//1. 这是一个销毁条件变量的接口
//2. cond : 需要销毁的条件变量的指针
#include<pthread.h>
int pthread_cond_wait( pthread_cond_t* cond,pthread_mutex_t* mutex);
//1. 这个是一个让线程去条件变量队列里等待的接口
//2. cond: 到那个条件变量的队列里排队 
//3. mutex:需要释放的锁(注意,到队列里排队前会先把线程所持有的锁给释放掉)
#include<pthread.h>
int pthread_cond_signal( pthread_conde_t * cond);
//1. 这是一个唤醒正在条件变量队列里排队的线程(这是一次单个唤醒)
//2. cond: 需要唤醒那个条件变量里的队列
#include<pthread.h>
int pthread_cond_broadcast( pthread_cond_t *cond);
//1. 这是一个唤醒条件变量里正在排队的线程接口(这是一次全部唤醒)
//2. cond: 你想唤醒那个条件变量里的线程
#include <pthread.h>
pthread_cond_t cond= PTHREAD_COND_INITIALIZER;
//1. 这是设置全局的条件变量
//2. 设置了全局的条件变量不需要初始化跟销毁
3.4使用

 在上面代码的基础上添加条件变,且在主线程对休眠的线程唤醒:

#include <iostream>
#include <pthread.h>
#include <unistd.h>
pthread_cond_t cond=PTHREAD_COND_INITIALIZER;//全局条件变量
//pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;//全局锁
using namespace std;int i = 100;
void *handler(void *args)
{pthread_mutex_t*mutex=static_cast<pthread_mutex_t*>(args);//参数类型转换// int a=10;while (true){if(i>0){pthread_mutex_lock(mutex);//上锁pthread_cond_wait(&cond,mutex);//到条件变量里的等待队列里等,同时解锁if (i > 0){//sleep(1);cout << i-- << endl;//sleep(1);// printf("Thread stack addresss is : %p\n", &i); // 打印a变量地址printf("thread id is: 0x%x\n",pthread_self());}pthread_mutex_unlock(mutex);//解锁}elsebreak;}return nullptr;
}
int main()
{pthread_mutex_t mutex;//锁变量创建pthread_mutex_init(&mutex,nullptr);//锁初始化pthread_t tid[3];for (int i = 0; i < 3; i++){pthread_create(tid + i, nullptr, handler, &mutex);}//主线程每隔一秒唤醒队列里休眠的线程while(true){pthread_cond_signal(&cond);sleep(1);}for (int i = 0; i < 3; i++){pthread_join(tid[i], nullptr);}pthread_mutex_destroy(&mutex);//锁销毁return 0;
}

注意这里为什么把到条件变量里等待要放在上锁之后,且为什么还要解锁?

 原因①:在获取锁之后进入等待队列确保在这里等待的线程都是由资格访问资源的线程,将来只要一被唤醒就可以直接去访问资源了!

②:解锁确保这个线程不携带着锁进入到等待队列,不然其他线程就无法再获取锁就会一直卡在外面,进入不了等待队列

③:因此获取锁跟条件变量属于依赖关系,出现条件变量的地方就是有锁的地方;

运行:

结果每个线程轮流访问临界资源并打印数据,解决线程饥饿问题;

四、锁的原理

4.1、锁是怎么是实现的?

画图理解:

我们把锁看做是一个等于1的变量a,线程加锁到解锁的过程是这样的:

①一个线程正在被cpu调度;

②线程调用获取锁的接口;

③线程去获取锁的过程实质是去内存中查看变量a是否为1,如果为1 代表这把锁存在,如果为0代表锁资源没有就绪;

④当变量为1时,cup会以一条汇编语句的方式(保证操作是原子性)将1跟线程所对应的寄存器里的0进行交换,注意:将1交换到寄存器中相当于已经交换到了线程的上下文当中!!而线程独立的两个资源中一个是线程的独立栈空间、一个是线程的上下文,这两个资源是其他线程看不到的,因此交换到上下文中的1(锁)独立存在于这个线程内,直到这个线程访问完资源并释放锁!

⑤释放锁过程跟上锁过程类似,以原子性的操作将线程上下文(独立)中的1跟内存(共享)的0交换,从而存在于线程独立的上下文中锁释放回共享内存中!

因此:因为锁也是公共资源,所以锁的设计一定是原子性的,保证操作一步完成!

原子性: 执行流完成某个动作要么不做,要么做完;

如何实现原子性操作:一条汇编语句的方式完成某个动作;

4.2 锁的应用(封装)

利用对象的生命周期来加锁、释放锁(RAII):

//定义一个lock类 
class lock{public:lock( pthread_mutex_t* mutex):mutex_(mutex){pthread_mutex_lock(mutex_);}~lock(){pthread_mutex_unlock(mutex_);}private:pthread_mutex_t * mutex_;};

应用到代码中:

运行:

结果一样;

五、死锁

5.1死锁概念

死锁指一组线程中的各个线程均占有不会释放的资源,因为互相申请被其他线程所占用不会释放的资源而处于一种永久等待的状态;

5.2死锁四个必要条件

互斥条件(前提条件),为什么要锁?因为避免访问同一资源数据不一致。怎么避免?互斥。什么是互斥?一个资源只能被一个执行流所使用;

②请求与保持条件:一个执行流因申请资源阻塞时,对已获得资源保持不放;

③不剥夺条件:一个执行流已获得的资源,在未使用完之前,不能强行剥夺; 

④循环等待条件:若干个执行流之间形成头尾相连的循环等待资源的关系;

以上四个条件缺一不可;

5.3解决死锁问题

编码注意:

· 加锁顺序一致

·避免锁未释放

理念:破坏四个必要条件之一

例如:

申请失败不死等(破坏循环等待条件)、申请失败释放自己(破坏请求与保持条件)、申请失败释放对方(破坏不剥夺条件)

今天的分享到这里,如果对你有所帮助记得点赞收藏+关注哦!!谢谢!!!

咱下期见!!!

http://www.xdnf.cn/news/585883.html

相关文章:

  • 基于FPGA控制电容阵列与最小反射算法的差分探头优化设计
  • 使用pm2 部署react+nextjs项目到服务器
  • (Java基础笔记vlog)Java中常见的几种设计模式详解
  • java接口自动化(四) - 企业级代码管理工具Git的应用
  • 理解全景图像拼接
  • 动态网页爬取:Python如何获取JS加载的数据?
  • Jenkins与Maven的集成配置
  • C++中的string(1)简单介绍string中的接口用法以及注意事项
  • Web前端开发 - 制作简单的焦点图效果
  • 单例模式的运用
  • UniApp+Vue3微信小程序二维码生成、转图片、截图保存整页
  • uniapp实现的简约美观的票据、车票、飞机票模板
  • ffmpeg 转换视频格式
  • 【Windows】FFmpeg安装教程
  • 「Python教案」运算符的使用
  • 中国计算机学会——2024年9月等级考试5级——第四题、森森快递(贪心+线段树)
  • JavaScriptAPIs学习day3--事件高级
  • 破局制造业转型: R²AIN SUITE 提效实战教学
  • Unity3D 异步加载材质显示问题排查
  • Python安全密码生成器:告别弱密码的最佳实践
  • TRC20代币创建教程指南
  • 解决 IntelliJ IDEA 配置文件中文被转义问题
  • ClickHouse核心优势分析与场景实战
  • 论文流程图mermaid解决方案
  • uni-app学习笔记八-vue3条件渲染
  • 打卡Day34
  • 绕距#C语言
  • IP大科普:住宅IP、机房IP、原生IP、双ISP
  • Keepalived 与 LVS 集成及多实例配置详解
  • React 与 TypeScript 极客园移动端