文章目录
- 一、线程
- (一)什么是线程
- (二)Linux下的多线程
- (三)总结
- (四)线程优点
- (五)线程缺点
- (六)线程异常
- (七)线程用途
- 二、Linux下的进程VS线程
- (一)进程和线程
- (二)进程的多个线程共享
- (三)进程和线程的关系
- 三、页表理解——虚拟到物理地址之间的转化
- (一)页表理解
- (二)页表的好处
- 1.进程虚拟地址管理和内存管理,通过页表+page进行解耦
- 2.节省空间:分页机制+按需创建页表
- 四、Linux线程控制
- (一)POSXI线程库
- 1.错误检查:
- 2.线程创建:pthread_create
- 3.获取进程ID:pthread_self
- 4.进程等待:pthread_join
- 5.进程终止:pthread_exit
- 6.pthread_cancel:进程取消
- 7.线程概念代码演示:
- 五、用户级线程概念
- (一)线程异常了怎么办?
- (二)理解 pthread_ t
- (三)线程栈
- 1.代码区有三类代码:
- 2.解释 pthread_create 创建线程的返回值pthread_t
- 3.线程局部存储
- 六、线程分离
- (一)概念
- (二) 例子
- 1.pthread_ detach(pthread_ self()); 新线程自我分离
- 2.pthread_ detach(tid1);主线程分离新线程
- (三) 线程分离可以理解为线程退出的第四种方式
一、线程
(一)什么是线程
- 在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是"一个进程内部的控制序列"。
- 一切进程至少都有一个执行线程
- 线程在进程内部运行,本质是在进程地址空间内运行
- 在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化
- 透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流
一般在Linux教材里面线程是这样定义的:
线程:是在进程内部运行的一个执行分支(执行流),属于进程的一部分,粒度要比进程更加细和轻量化。
按照上面教材的这种说法,现在有一些问题:
一个进程内可能存在多个线程?
这是可能呢。
进程:线程 = 1 : N
在OS中存在这么多的线程,那么OS要不要管理线程呢?如何管理呢?
答案是肯定要管理的,六字真言:先描述,再组织。
因此我们就可以得出一个推论:线程也应该要有线程控制块TCB
的确,我们window下的多线程就是这样子做的。
(二)Linux下的多线程
但是我们Linux下的多线程并没有像上面说的那样通过创建数据结构,然后通过管理数据结构从而达到管理线程的目的。
那我们Linux下的多线程是怎么做的呢?
我们先来回顾一下进程,我们知道创建一个进程,我们需要为它创建一些列的数据结构,比如说:PCB(进程控制块)、mm_struct(进程地址空间)、页表和file_struct等等。
CPU此时看到的PCB是<=我们之前讲的PCB的概念的,在CPU看来一个PCB就是一个需要被调度的执行流。
这就是我们Linux下的线程,Linux中并没有像windows下为线程专门设计TCP,Linux的线程是用进程模拟的,而是使用进程PCB来模拟线程。
那Linux这样做有什么好处呢?
不用维护复杂的进程和线程的关系,不用单独为线程涉及任何算法,直接使用进程的一套相关的方法,OS只需要聚焦在线程间的资源分配上就可以了。
现在我们再来理解一下上面教材说的:线程是在进程内部运行的一个执行分支,这里的内部是什么意思呢?,那什么又叫做一个执行分支呢?
这里的内部指的是线程是在进程的虚拟地址空间中运行的。
执行分支指的是CPU调度的时候只看PCB,每一个PCB曾经被指派过指向方法和数据,CPU是可以直接调度的。
在今天了解了Linux下的线程之后,我们又该如何理解我们之前讲的进程呢?
下面用绿色方框圈起来的内容,我们将这个整体叫做进程。
(三)总结
- 之前的进程,内部只有一个执行流,今天的进程,内部可以具有多个执行流。
- 创建进程的"成本非常高",成本:时间+空间,创建一个进程要使用的资源是非常多的(0-1),我们从内核视角来看进程就是承担分配系统资源的基本实体!!
- 线程就是CPU调度的基本单位,承担进程资源的一部分的基本实体,进程划分资源给线程。
- 总得来说 Linux下的线程就是轻量级进程。
曾经:进程——内核数据结构+进程对应的代码和数据
现在:进程——内核视角:承担分配系统资源的基本实体 (进程的基座属性),即:向系统申请资源的基本单位!
内部只有一个执行流 task_struct 的进程——单执行流进程
内部有多个执行流 task_struct 的进程——多执行流进程
线程(执行流)是调度的基本单位!
下面紫色框起来的进程PCB,虚拟内存,页表,内存中的数据和代码,这一组资源的集合叫做一个进程。
(四)线程优点
- 创建一个新线程的代价要比创建一个新进程要小得多。
- 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
线程占用的资源要比进程少很多。 - 能充分利用多处理器的可并行数量。
- 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务。
- 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现。
- I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。
- 使用多线程可以更加充分利用cpu资源,使任务处理效率更高,进而提高程序响应,即:耗时的操作使用线程,提高应用程序响应
- 对于多核心cpu来说,每个核心都有一套独立的寄存器用于进行程序处理,因此可以同时将多个执行流的信息加载到不同核心上并行运行,充分利用cpu资源提高处理效率,即:多CPU系统中,使用线程提高CPU利用率,CPU线程调度程序中的不同线程来共同执行整个程序,并非让CPU线程独立执行程序.
(五)线程缺点
- 性能损失
一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
- 健壮性降低
编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
- 缺乏访问控制
进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
- 编程难度提高
编写与调试一个多线程程序比单线程程序困难得多
(六)线程异常
- 单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃。
- 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出。
(七)线程用途
- 合理的使用多线程,能提高CPU密集型程序的执行效率
- 合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)。
二、Linux下的进程VS线程
(一)进程和线程
- 进程是资源分配的基本单位
- 线程是调度的基本单位
- 线程虽然共享进程数据,但也拥有自己的一部分数据:
线程ID
一组寄存器(保护CPU调度时存放在寄存器中的临时数据)
栈(保护线程运行时所形成的临时数据)
errno
信号屏蔽字
调度优先级
(二)进程的多个线程共享
因为进程和线程是在同一个地址空间的,因此代码段(Text Segment)、数据段(Data Segment)都是共享的:
- 如果定义一个函数,在各线程中都可以调用
- 如果定义一个全局变量,在各线程中都可以访问到
除此之外,各线程还共享以下进程资源和环境:
- 文件描述符
- 每种信号的处理方式(SIG_IGN、SIG_DFL或者自定义的信号处理函数)
- 当前工作目录
- 用户id和组id
(三)进程和线程的关系
在之前我们接触到的是单线程进程或者多个单线程进程。
- 在linux中进程比线程安全的原因是每个进程有独立的虚拟地址空间,每个进程有自己独有的数据,具有独立性,不会数据共享这个说法是错误的,太过宽泛与片面.
- 多进程之间的数据共享比多线程编程复杂,线程之间的通信简单(共享地址空间和页表信息,因此传参以及全局数据都可以实现通信),而不同进程之间的通信更为复杂,通常需要调用内核(系统调用)实现
- 多线程的创建,切换,销毁速度快于多进程,因为线程之间共享了进程中的大部分资源,因此共享的数据不需要重新创建或销毁,因此消耗上低于进程,反之也就是速度快于进程.
- 对于大量的计算优先使用多线程。大量的计算使用多进程和多线程都可以实现并行/并发处理,而线程的资源消耗小于多进程,而稳定相较多进程有所不如,因此还要看具体更加细致的需求场景
- 一个进程至少有一个线程正确,但是“一个程序至少有一个进程”是错的,因为程序是静态的,不涉及进程,进程是程序运行时的实体,是一次程序的运行
- 线程自己不拥有系统资源,因为 进程是资源的分配单位,所以线程并不拥有系统资源,而是共享使用进程的资源,进程的资源由系统进行分配
- 任何一个线程都可以创建或撤销另一个线程。
三、页表理解——虚拟到物理地址之间的转化
(一)页表理解
虚拟地址在被转化的过程中,不是直接转化的
虚拟地址是32位的:32bit 分成 10+10+12
0101 0101 00 0100 0111 11 0000 1110 0101
XXXX XXXX xx yyyy yyyy yy zzzz zzzz zzzz
虚拟地址的前10位在一级页表——页目录中找对应的二级页表;找到对应的二级页表后,中间10位在二级页表中找对应的page的起始地址(物理内存);找到对应的page的起始地址后,后12位作为偏移量在物理内存中的一个page(4KB)中找对应数据的地址,因为后12位有2^12=4096字节=4KB,正好物理内存管理单位是一个page,一个page是4KB,则后12位正好可以覆盖一个page的所有的地址。找到地址后CPU读取物理内存的数据。
(二)页表的好处
1.进程虚拟地址管理和内存管理,通过页表+page进行解耦
2.节省空间:分页机制+按需创建页表
页表也要占据内存,页表分离了,可以实现页表的按需创建,比如页目录的第3个地址从来没使用过,就可以不创建对应的二级目录,需要时再创建。一个页表大小是232/212=2^20字节(页目录和二级页表)
四、Linux线程控制
(一)POSXI线程库
- 与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头的
- 要使用这些函数库,要通过引入头文<pthread.h>
- 要链接这些线程函数库时要使用编译器命令的“-lpthread”选项
ps -aL (all light)查看所有的轻量级进程
LWP——light wait process:LWP就是轻量级进程,描述的是一个进程中的一个pcb
1.错误检查:
- 传统的一些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误。
- pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做),而是将错误代码通过返回值返回。
- pthreads同样也提供了线程内的errno变量,以支持其它使用errno的代码。对于pthreads函数的错误,建议通过返回值业判定,因为读取返回值要比读取线程内的errno变量的开销更小。
2.线程创建:pthread_create
创建线程的函数叫做pthread_create
功能: 创建一个新的线程
函数原型如下:
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,创建失败返回错误码
3.获取进程ID:pthread_self
创建线程的函数叫做pthread_self()
功能: 获得该线程自身的ID
函数原型如下:
pthread_t pthread_self(void);
4.进程等待:pthread_join
功能:线程退出的时候,一般必须要进行join等待,如果不进行join,就会造成类似于进程那样的内存泄露问题。(即:作用是:释放线程资源——前提是线程退出了。并获取线程对应的退出码)
函数原型如下:
int pthread_join(pthread_t thread, void **value_ptr);
参数说明: thread:线程id
value_ptr:它指向一个指针,后者指向线程的返回值。(join 不需要退出信号)
返回值:成功返回0;失败返回错误码
调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的
终止状态是不同的,总结如下:
- 如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。
- 如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数
PTHREAD_ CANCELED。 - 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参
数。 - 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数。
5.进程终止:pthread_exit
终止线程。线程终止——只考虑正常终止
(1)pthread_exit 对比 exit
exit(1):代表退出进程,任何一个 主/新线程调用exit,都表示整个进程退出。pthread_exit()仅仅是代表退出线程。
(2)线程退出有3种:
-
线程退出的方式,return —— return (void*)111;
-
线程退出的方式,pthread_exit —— pthread_exit((void*)1111);
-
线程退出的方式:线程取消请求,pthread_cancel —— pthread_ cancel(tid);
void pthread_exit(void *retval); retval:线程退出码
6.pthread_cancel:进程取消
函数原型
int pthread_cancel(pthread_t thread);
参数说明: thread:线程id
返回值:成功返回0;失败返回错误码
7.线程概念代码演示:
#include <iostream>
#include <cstdio>
#include <cassert>
#include <pthread.h>
#include <unistd.h>
using namespace std;
int g_val = 0;
string fun() {
return "我是一个独立的方法!";
}
void *thread_routine(void *args) {
const char* name = (const char *)args;
while (true) {
fun();
cout << "我是新线程, 我正在运行! name: " << name << " : "<< fun() << " : " << g_val++ << " &g_val : " << &g_val << endl;
sleep(1);
}
}
int main() {
pthread_t tid;
int n = pthread_create(&tid,nullptr,thread_routine,(void*)"thread one");
assert(n == 0);
(void)n;
while (true) {
char tidbuffer[64];
snprintf(tidbuffer,sizeof(tidbuffer),"0x%x",tid);
cout << "我是主线程, 我正在运行!, 我创建出来的线程的tid: " << tidbuffer << " : " << g_val << " &g_val : " << &g_val << endl;
sleep(1);
}
return 0;
}
五、用户级线程概念
据操作系统内核是否对线程可感知,可以把线程分为内核线程和用户线程。
用户级线程由应用程序所支持的线程实现,内核意识不到用户级线程的实现。内核级线程又称为内核(系统)支持的线程。
(一)线程异常了怎么办?
- 线程异常了——>整个进程整体异常退出。
- 线程异常==进程异常
- 线程会影响其他线程的运行——新线程会影响主线程main thread——健壮性/鲁棒性较低。
(二)理解 pthread_ t
本质上是一个地址。
- 线程是一个独立的执行流
- 线程一定会在自己的运行过程中,产生临时数据(调用函数,定义局部变量等)在新线程中修改全局变量后,新线程和主线程都能看到被修改后的结果
- 线程一定需要有自己的独立的栈结构。
(三)线程栈
我们使用的线程库,用户级线程库,库的名字叫pthread。
1.代码区有三类代码:
- 你自己写的代码。
- 库的接口代码。(例如动态库libpthread. so会写入内存,通过页表映射到进程的共享区,代码区的库接口代码通过跳转到共享区执行完库中的代码,然后再跳转回代码区继续执行)。
- 系统接口代码。(通过身份切换 用户—>内核 执行代码)。
所有的代码执行,都是在进程的地址空间当中进行执行的
2.解释 pthread_create 创建线程的返回值pthread_t
用户要用线程,但是OS没有线程的概念,libpthread. so线程库起承上启下的作用。
共享区内:
线程的全部实现,并没有全部体现在OS内,而是OS提供执行流,具体的线程结构由库来进行管理。库可以创建多个线程->库也要管理线程->管理:先描述,在组织
struct thread_ info
{
pthread_ t tidh .
void *stack; //私有栈
……
}
libpthread. so线程库映射进共享区中。
创建线程时,线程库中也会创建一个 结构体struct thread_ info叫做线程控制块, 线程控制块内部是描述线程的信息,内部有一个指针指向mm_struct用户空间的一块空间——线程栈。
创建线程成功后,返回一个pthread_t类型的地址,pthread_t类型的地址保存着我们共享区中对应的用户级线程的线程控制块的起始地址!
结论:主线程的独立栈结构,用的就是地址空间中的栈区;新线程用的栈结构,用的是库中提供的栈结构(这个线程栈是库维护的,空间还是用户共享区提供的)
Linux中,线程库用户级线程库,和内核的LWP是1:1(
LWP(类比PID)——light wait process:轻量级进程编号。LWP=PID的执行流是主线程,俗称进程)。
3.线程局部存储
线程库中的结构体struct thread_ info,内部是描述线程的信息,struct thread_ info中还有一个叫做线程局部存储的区域,作用:可以把全局变量私有化
正常情况全局变量是多个线程可以同时修改的:
加上__thread,把全局变量拷贝给每个进程各一份,使全局变量私有化,各自修改自己的全局变量。
六、线程分离
(一)概念
- 默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。
- 线程分离:如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。
int pthread_detach(pthread_t thread);
可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离
int pthread_detach(pthread_self())
joinable和分离是冲突的,一个线程不能既是joinable又是分离的。
(二) 例子
1.pthread_ detach(pthread_ self()); 新线程自我分离
2.pthread_ detach(tid1);主线程分离新线程
(三) 线程分离可以理解为线程退出的第四种方式
- 线程分离分为立即分离,延后分离,要保证线程还活着。线程分离意味着,我们不在关心这个线程的死活。线程分离可以理解为线程退出的第四种方式——延后退出。
- 主线程的退出,并不会导致进程退出,也不会影响其他线程的运行。进程中的所有线程退出了,进程才会退出。—— 一般我们分离线程,对应的main thread不要退出(常驻内存的进程)