Linux:多线程---线程控制(线程创建&&线程等待&&线程终止)

文章目录

      • [1. 线程创建](#1. 线程创建)
      • [2. 线程等待](#2. 线程等待)
      • [3. 线程终止(退出)](#3. 线程终止(退出))
      • [4. 原生线程库](#4. 原生线程库)
        • [4.1 重新理解原生线程库](#4.1 重新理解原生线程库)
        • [4.2 深度剖析原生线程库](#4.2 深度剖析原生线程库)
  • 序:距离上一章的更新已经过去16天了,期间忙于其他的事,同时也有写懒惰,现在开始恢复更新,希望大家多多支持,在上一章中,我们对线程的基本概念有了一个认识,知道了执行流,线程与进程的关系,知道了线程的优缺点,异常,用途,以及接口的使用等等,就像之前对进程的学习一样,我们先知道了进程的概念,而后就是进程创建,进程终止和进程等待,统称进程控制,而今天我们将从线程控制出发,了解线程创建,线程终止以及线程等待!!!

1. 线程创建

功能:创建一个新的线程

int pthread_create(pthread_t * thread, const pthread_attr_t * attr, void * ( * start_routine)(void * ), void* arg);
参数:
thread:返回线程ID
attr:设置线程的属性,attr为NULL表示使用默认属性
start_routine:是个函数地址,线程启动后要执行的函数
arg:传给线程启动函数的参数
返回值:成功返回0;失败返回错误码
错误检查:
传统的一些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误。
pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做)。而是将错误代码通过返回值返回
pthreads同样也提供了线程内的errno变量,以支持其它使用errno的代码。对于pthreads函数的错误,建议通过返回值业判定,因为读取返回值要比读取线程内的errno变量的开销更小
线程ID及进程地址空间布局:
pthread_ create函数会产生一个线程ID,存放在第一个参数指向的地址中。该线程ID和前面说的线程ID不是一回事。
前面讲的线程ID属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程。
pthread_ create函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID,属于NPTL线程库的范畴。线程库的后续操作,就是根据该线程ID来操作线程的。
线程库NPTL提供了pthread_ self函数,可以获得线程自身的ID:

复制代码
thread_t pthread_self(void);

2. 线程等待

新线程的创建,也需要主进程等待,否则会出现问题(类似于僵尸进程)
等待线程:

其中返回值,成功为0,失败就会返回错误码。线程的接口的返回值基本上都是返回错误码,不去设置全局的错误码,这样就保证了每个线程都能有自己的错误码!!!
第一个参数是表示是对哪个线程进行等待
第二个参数是二级指针。如果不用知道线程的返回值就设置为nullptr就行
pthread_join等待的时候,默认是阻塞等待的!!!

问题一:如果一个线程退出了,如果我们要知道这个线程他退出时的返回值,该怎么获取?

通过pthread_join函数的第二个参数!!!由于这个线程结束时的返回值void*,用户无法直接获取,该返回值在内核部分里,所以,用户要是想拿到该返回值就只能调用pthread_join函数!!!而用户想要拿到该返回值,就要先定义一个void* 的变量,将这个变量的地址传入pthread_join中,如果有pthread_join函数,那么当线程在退出时会将自己的返回值传给* retval,此时retval就算拿到了线程退出时的返回值了,然后用户自己定义的变量x就能拿到该线程退出时的返回值!!!

问题二:我们在学进程等待的时候,我们知道一个进程退出有运行正常退出,运行错误退出和运行异常,三种退出,我们还知道如果是异常退出还会有异常信号,那为什么线程等待的pthread_join没有考虑异常呢?难道线程就不会异常退出吗?

做不到!!!因为一但线程出异常,整个进程也会挂掉,其他线程也都会挂掉!!!所以线程不用考虑异常问题,只需要他的主进程考虑就行了!!!

3. 线程终止(退出)

在进程中,如果我们要退出进程,我们可以使用return和exit两种方式,但是在线程中,线程直接调用exit,就会导致整个进程都会挂掉,因为exit是用来终止进程的,不能用来直接终止线程!!!线程可以通过return来终止。
此外,我们还能通过调用pthread_exit(void ** retval)来终止进程

线程的终止:

  1. return(void*)
  2. pthread_exit(void*)
    main进程,当线程存在时,可以通过调用pthread_cannel(pthread_t tid)函数来关闭某个已经存在的线程,此时,该线程的线程等待的返回值变成-1,即PTHREAD_CANNLED((void*)(-1))。

4. 原生线程库

目前,我们Linux中的pthread库是原生线程库。而且我们知道c++11语言本身也已经支持多线程了
C++11线程库vs原生线程库
如果g++中没有加入-lpthread选项就会报错,所以C++11在g++时,还是要加-lpthread!!!可以看出C++11的多线程的底层还是Linux中的原生线程库!!!为什么说C++语言具有夸平台性,因为,在Linux环境下,C++下载的是linux版本的库,在Windows环境下,C++下载的是Windows版本的库!!!

4.1 重新理解原生线程库

我们知道该函数接口可以查询当前线程的线程tid,当我们将该线程的id以16进制打印出来后是一个地址!!!而在后面的部分中我将在逐步解析原生线程库中解答这一点。
之前我们知道,Linux中是没有线程的概念的,只有轻量型进程的概念,那么,要创建一个轻量型进程就要调用对应的系统接口---Clone函数!!!

Fork的底层就是clone函数,创建轻量型进程的接口,当然,我们并不会去调用这个接口,因为这个接口的参数太多了!!!只要知道clone函数是个什么东西,参数是什么,用来干什么的就行了。
第一个参数:一个函数指针,指向新的执行流(回调函数)。
第二个参数:创建新线程时需要传入自定义的一个栈
第三个参数:要不要让地址空间实现共享
其他参数本章不讨论,感兴趣的小伙伴可以自行去学习。
这个clone系统接口就被pthread线程库封装了,我们用的就是pthread_create、pthread_join...

问题一:我们用的原生线程库,要不要加载到内存中,如果加载的话,又加载到哪里??

原生线程库是一个动态库,肯定要加载到内存中!!!经过页表,将pthread库映射到共享区!!!所以后续的讨论将都是基于内存中谈的!!!
线程的概念是由库来维护的!!!
既然线程的概念是库来维护的,那么线程的PID,时间片的分配等等,这也注定了线程库要维护多个线程属性集合,线程库也必须管理这些线程!!!
怎么管理呢?
先描述,再组织!!!

4.2 深度剖析原生线程库

该线程的tid换成16进制后存储的就是线程库中的维护的线程控制块的信息的起始地址!!!(每一个线程的库级别的tcb的起始地址叫做线程的tid!!!( 由于是虚拟地址,所以可以直接访问))每创建一个线程,就是在线程库的共享区内开辟一段空间将对应的信息填充好(独立栈,线程的回调函数),对于每一个线程tcb结构体,线程库会像数组一样将这些tcb管理好,其中线程的退出结果就是存储在线程局部存储区域的,这也就是为什么我们必须要调用pthread_join函数来获取返回值,而不能直接获取,因为该返回值在地址空间的共享区内,是系统资源!!!所以只能通过系统调用来获取。
每一个线程都有自己的调用链,也就注定了每一个线程必须有独立的调用链所对应的栈帧结构,其中的线程的独立栈中会保留任何执行流运行过程中的所有临时变量,比如传参时形成的临时变量比如返回时的返回值,比如返回时的返回地址,所以,每一个线程,必须有自己的独立的栈帧结构,其中主线程直接调用地址空间当中提供的栈即可。
轻量型进程的产生:首先是在库里面为新线程创建线程控制块,申请对应的空间,然后将该线程控制块的起始地址作为该线程的tid,其中,该线程控制块中包含一段默认大小的空间叫做线程栈,当上层准备创建执行流后,系统就会调用clone系统接口,将回调函数传入第一个参数,更重要的是将线程栈传递给第二个参数,其中clone产生的临时数据都会压入线程库在应用层的栈结构当中,换句话说,所有对应的非主线程,他的栈都在库当中进行维护,即在共享区当中维护,具体来讲是在pthread库中,tid指向用户的tcb中!!!
所有进程创建的线程都在共享区的动态库中进行管理。
执行流:如果cpu看到的task_struct是线程的,那么执行流就是等于线程的,如果cpu看到的task_struct是进程的,那么执行流就大于线程。
用户级线程和内核级轻量级进程(LWP)一起构成了Linux线程
每一条执行流的本质就是一条调用链,每一个调用链都会有自己独立的栈结构!
其实线程和线程之间,几乎没有秘密,线程的栈上的数据,也是可以被其他线程看到并访问的。全局变量是被所有的线程同事看到并访问的!

问题一:如果线程想要一个私有的全局变量呢????

在全局变量__thread +类型 +名字(例如:__thread int g_val=100;),就能实现!!!(线程的局部存储!!!)(__thread叫做编译选项)线程的局部存储,只能定义内置类型,不能用来修饰自定义类型

总结:

本篇从三个方向来讲述了线程控制,分别讲解了线程创建、线程等待和线程终止,分别从接口以及参数的意义和功能的角度来了解,以及最后深入原生线程库,了解用户级线程与内核轻量型进程的关系。

相关推荐
xcl09253 小时前
北京24小时自助健身房系统软件开发实战:从架构到部署全流程指南
java·spring boot·架构
一 乐5 小时前
动漫书销售商城|基于springboot + vue动漫书销售商城(源码+数据库+文档)
java·数据库·vue.js·spring boot·毕业设计
卓怡学长5 小时前
w214基于jsp知道特产网
java·intellij-idea
步行cgn5 小时前
Spring 注解使用详解
java·spring
其实防守也摸鱼6 小时前
Fastjson 反序列化漏洞(JNDI注入)
android·数据库·安全·oracle·自动化·fastjson·反序列化
一条小小yu6 小时前
Spring IoC的理解
java·后端·spring
落魄实习生6 小时前
Agent Scope Java 2.x 系列【7】工具使用
java·开发语言·ai
旺仔学长 哈哈6 小时前
springboot钓鱼爱好者交流平台APP设计与实现
java·spring boot·mysql·充电桩管理系统
狗凯之家源码网7 小时前
微博图床 PHP 系统搭建与实战应用指南
android·开发语言·php
夜听莺儿鸣7 小时前
502-003_Linux 中断与异常(一):从Linux角度理解中断与异常
linux·中断与异常·linux中断