线程(下)【Linux操作系统】
文章目录
- 线程控制
- 线程共享进程地址空间中的所有数据
- 线程会瓜分进程的时间片
- 线程相关库函数
- 库函数:pthread_create
- 库函数:pthread_self
- 库函数:pthread_join
- 库函数:pthread_exit
- 库函数:pthread_cancel[尽量少用]
- 库函数:pthread_detach
- 为什么Linux线程相关的函数是库函数,而不是系统调用?
- 线程的状态和回收
- 线程终止
- 线程分离
- 线程库的理解
- 线程id
- 线程TCB的存放和内容
- TCB的存储
- TCB中的内容
- 线程的栈
- 线程局部存储
线程控制
线程共享进程地址空间中的所有数据
全局区,代码区,堆区,共享区,栈区都是共享的
没错栈区也是共享的
只要线程a能拿到线程b函数中的栈区变量的地址,就可以对它进行修改和访问
而且修改之后,线程b看到的也是改变了之后的【但是强烈不建议这么做
】
线程会瓜分进程的时间片
即:
一个进程的时间片是10ns
它一共有5个线程,那么每个线程的时间片就是2ns
为什么呢?
操作系统进行调度时,是以线程为单位的
而且承担资源分配的基本实体是进程,进程是资源的容器,线程瓜分进程资源,而时间片也是资源!!!
所以,必须给每个线程分时间片,这样操作系统调度线程时,才知道线程什么时候时间片耗尽
而且:
如果线程的时间片和它所属的进程一样长,那么一个进程就可以通过创建更多线程,来延长自己占用CPU的时间
这显然对其他进程不公平
这和分时操作系统的公平调度理念不符
线程相关库函数
库函数:pthread_create
-
头文件:
pthread.h
-
返回值:int
①成功,就返回0
②失败,就返回错误码 -
参数表:
-
①
pthread_t *thread
:输出型参数,用于获取线程tid,本质也是一个整型 -
②
const pthread_attr_t *attr
:线程属性 -
③
(void*)(*p)(void*)
:指向给这个线程分配的函数的函数指针 -
④
void*arg
:传给分配给线程的函数的参数
为什么是void * 类型?
就是为了支持接收任意类型的参数,变量,数组,对象都可以传递过去
这样就可以一次传递多个信息,也可以更好地支持C和C++混合编写
我们可以定义一个任务类,里面存储一些基本信息和分配给这个新线程的任务函数
即设计一个任务类,里面的成员变量就是任务函数,以及任务函数对应的参
-
-
作用:创建一个线程
库函数:pthread_self
-
头文件:
pthread.h
-
返回值:
pthread_t
①成功,该线程的tid
②失败,就返回-1 -
作用:获取唯一标识一个线程的tid
库函数:pthread_join
-
头文件:
pthread.h
-
返回值:int
①成功,0
②失败,就返回错误码 -
参数表:
-
①
pthread_t thread
:要等待的线程的tid -
②
void**retval
:因为分配给新线程的函数的返回值为void*
类型的,所以以输出型参数的方式获取这个返回值就得是void**
类型的
-
-
注意:
pthread_join是阻塞等待的从线程的
一般是主线程等待从线程
库函数:pthread_exit
-
头文件:
pthread.h
-
返回值:void
-
参数表:
void*retval
:线程的退出信息 -
作用:让调用这个函数的线程退出
库函数:pthread_cancel[尽量少用]
-
头文件:
pthread.h
-
返回值:int
①成功,0
②失败,就返回错误码 -
参数表:
pthread_t thread
:要取消的线程的tid -
注意:
- ①一个线程能被取消的前提是,这个线程已经启动了
- ②被取消的线程也必须被回收(不然会内存泄露),而且退出码必定是-1
库函数:pthread_detach
-
头文件:
pthread.h
-
参数表:
pthread_t thread
:要设置成要把被等待状态设置成detach的线程的tid -
作用:
把指定线程的被等待状态设置成detach
为什么Linux线程相关的函数是库函数,而不是系统调用?
因为在Linux中没有给线程先描述再组织
Linux内核中的线程是使用LWP模拟实现的
所以
Linux内核就只给我们提供了创建LWP的系统调用接口(即系统调用clone)操作线程的各种系统调用通通没有
用户不愿意直接去使用创建LWP的系统调用,因为使用这个系统调用还得去了解Linux的LWP等相关知识,成本太高了
所以
Linux就让库封装了LWP相关的系统调用得到了给用户提供了包括线程的各种操作的的库[库的名字就叫pthread]
封装成库之后,用户就只需要知道线程的相关概念就可以轻松使用库函数了
所以
Linux中,如果使用了线程相关的函数,那么编译时都要链接pthread动态库,pthread是Linux实现的自带的库,是原生线程库
所以:
其他任何语言想要在Linux平台上支持线程,就必须封装Linux的pthread库
线程的状态和回收
Linux中的线程就是用LWP(task_struct)模拟实现的
所以一个线程对应一个PCB
而PCB里面就存储了状态信息
所以线程的状态管理,完全可以复用进程的状态管理
从线程也需要被主线程等待和回收(使用pthread_join)
为什么❓
-
①因为线程的退出信息,在线程库中它自己的TCB中维护着,而TCB关联内核的LWP的生命周期
-
②因为主线程也要知道其他线程把任务执行地怎么样
所以如果不等待,即使线程退出了,操作系统也不敢释放它的LWP,就会出现与进程类似的僵尸问题
线程终止
会让线程终止的方法一共3种
-
①分配给线程的函数
return
,该线程会退出 -
②线程调用了
pthread_exit
,该线程就会退出 -
③其他线程(一般是主线程)调用
pthread_cancel
取消线程
线程分离
主线程也可以不等待新线程,自己干自己的事
但是线程并不提供非阻塞等待
而是通过修改新线程的被等待状态,来做到
线程有两种被等待的状态
-
①
joinable
:线程需要被join等待和回收(新线程默认是joined状态) -
②
detach
:线程分离(这种状态的线程执行完了就直接退出,即使主线程调用pthread_join
专门去等待分离状态的线程,也会因为检测到它是分离状态,主线程直接返回,不会阻塞)
此时要注意一点:
如果线程a和主线程分离了,如果线程a还在运行,主线程就退出了
这个时候可能:[不同的线程库实现不一样]
- ①因为主线程退出,进程就直接也退出了
- ②主线程退出,但是进程没退出
注意:
多执行流时,尽可能保证主执行流最后退出=
线程库的理解
线程id
我们使用的pthread库里面的接口的时候,使用的事线程id来操纵线程
但是线程id并不是Linux内核中的LWP,而是线程库pthread自己维护的
线程id的本质是地址,是线程库维护的这个线程对应的结构体(tcb)变量的起始地址
创建线程的时候,不仅Linux内核中会创建一个LWP描述线程的内核级属性,线程库pthread中也会创建一个TCB结构体变量来维护线程的用户级属性
即线程库中,会对线程进行先描述,再组织
为什么❓
-
①LWP是描述轻量级进程的,但是用户使用的是线程,所以用户需要的是线程的属性,而不是轻量级进程的属性
即LWP并不能非常好地描述用户所需的线程的属性
(比如:LWP中没有维护这个线程的栈大小,而线程库TCB中维护了) -
②为了解藕
因为用户使用的是线程库,线程库创建出来的线程是用户级线程
用户级线程不宜和内核级线程共用结构体和数据结构,这样耦合度太高,而且还要使用系统调用陷入内核
线程库哪里来的空间对线程先描述再组织?
线程库维护进程的线程使用的空间是进程自己申请的物理内存(即使用进程的共享区内存来存储
)
因为这样描述线程属性的结构体变量才有虚拟地址,执行流执行线程的库函数的时候,才能找到并访问到对应的数据
所以就可以做到:
不同的进程,使用同一个线程库,但线程库中维护的线程是不一样的
但是线程库的代码区的代码是一样的
线程TCB的存放和内容
TCB的存储
线程库中TCB的存储是在共享区中开辟一块内存,把所有TCB以及对应的连续存储在一起的
TCB中的内容
- ①对应的轻量级进程的LWP
- ②对应进程的pid
- ③void*result:分配给自己这个线程的函数的返回值
- ④用户分配给自己这个线程的入口函数的地址,以及它对应的参数
- ⑤自己这个线程对应的栈的起始虚拟地址,以及栈的大小
- ⑥线程对应的线程局部存储的起始地址,及其大小
- ⑦标记线程是否分离的bool类型的变量
线程的栈
线程的栈是独立的,分两种
-
①进程地址空间中所谓的栈区,其实是专门给主线程用的栈区,大小是不固定的,可以扩容(
当然也受到mm_struct的区域划分限制
) -
②新线程的栈区是在创建新线程的同时,使用mmap在共享区开辟的一块的大小固定(一般默认是8MB,如果用满了8MB还用就会栈溢出)的内存
所以才说线程的栈是独立的
线程局部存储
一个全局变量b本来是被所有线程共享的,所有线程访问这个的全局变量b时,访问的虚拟地址是同一个
如果定义时,给全局变量b前面加一个__thread
那么这个全局变量b就不是被所有线程共享的了,不同线程访问这个变量b时,访问的虚拟地址不同
这是怎么做到的?
如果一个变量a定义时加了__thread,编译器编译的时候,如果创建了新线程,就会在这个线程的线程局部存储中开辟空间存储一份这个变量a
以后这个线程访问这个变量a的时候,就只访问自己线程局部存储中的变量a
注意:
__thread只能修饰内置类型的变量
线程局部存储的作用:
-
①不同线程对应一个独立的错误码,比如C标准库提供的errno
-
②缓存线程属性等访问频繁的数据,提高访问速度
比如:
可以加__thread
定义一个全局的线程id,在对应线程里面初始化之后
这样以后使用线程id,就不需要在调用pthread_self
,直接用它,不同的线程看到的也是自己的id