目录
[2.Linux 系统对线程的实现](#2.Linux 系统对线程的实现)
[4.Linux 线程的周边概念](#4.Linux 线程的周边概念)
[a.pthread_create ------ 原生线程库](#a.pthread_create —— 原生线程库)
[② pthread 线程库的底层原理](#② pthread 线程库的底层原理)
[b.thread ------ C++线程库](#b.thread —— C++线程库)
[a. return ------ 线程自然返回](#a. return —— 线程自然返回)
[b. pthread_exit (void* retval)](#b. pthread_exit (void* retval))
[c. pthread_cancel (tid)](#c. pthread_cancel (tid))
[d. 信号终止](#d. 信号终止)
[e. 主线程退出](#e. 主线程退出)
[①pthread_jion ------ 原生线程库](#①pthread_jion —— 原生线程库)
[②join ------ C++11线程库](#②join —— C++11线程库)
一.线程的概念
1.什么是线程?
课本概念:线程是比进程更加轻量化的一种执行流 ,或者说,线程是在进程内部 的一种执行流。
我的理解:进程 是承担分配系统资源的基本实体, 而线程 则是CPU调度的基本单位。
进程和线程的联系与区别,图解如下:
2.Linux 系统对线程的实现
a.对于Linux系统,线程复用了进程的数据结构和管理算法(如:线程间切换、线程的调度、阻塞、运行、挂起)
b.在Linux系统中,线程是在进程"内部"执行的。由于任何执行流的执行都需要有资源作依托,而程序地址空间又是进程的资源分配窗口,所以可以说,线程的资源分配 ,本质就是分配地址空间范围!!
c.在Linux系统中,由于进程的整体资源被线程分配得更少了,所以线程的执行粒度要比进程更"细"。
线程比进程要更轻量化体现在什么方面??
①由于线程所用到的资源更少,所以,线程的创建和释放更加轻量化
②相比于进程间切换,线程间切换更轻量化
线程切换较进程切换效率高的原因??
①cache缓存(主要原因)
线程间切换时,不需要更新cache里面的热数据。CPU访问内存上的代码是一件较为耗时的事,而根据局部性原理,CPU会将内存上所访问位置周围的多条代码一次全部拷贝到CPU的cache(硬件级的缓存,集成在CPU内部)上,从而减少CPU访问内存的次数,提高效率。而cache在缓存时是以进程为单位的,所以线程间切换不需要刷新cache,而进程间切换需要!!
②寄存器的刷新(次要原因)
相较于进程间切换,线程间切换时,刷新的CPU寄存器更少 。CPU上有特殊的寄存器指向地址空间、页表和上下文的保存,进程间切换时,这部分的寄存器会被覆盖刷新;而线程切换时,只需覆盖一些程序运行时产生临时数据的寄存器即可。换句话说,进程间切换,需要刷新CPU上的所有寄存器;而线程间切换,只需要刷新少量的、特殊的寄存器。
3.线程的理解(原理)
创建线程和创建进程的目的都是让它们各自做各自的事,二者有什么区别?
创建线程,它是在进程内部创建的,OS不会单独为线程创建地址空间、页表、文件描述符表...等资源,仅是多创建了一个task_struct结构体,线程会与主线程共享同一套地址空间等资源;
创建进程,OS不仅会为其创建一个task_struct,还会为其开辟地址空间、页表等大量资源,所以说,进程是承担分配系统资源的基本实体,进程的创建要消耗更多的系统资源,它是很"重"的!!
ps -aL 查看线程的指令
LWP (Light Weight Process , 轻量级进程,即线程)
总结,什么是线程??
Linux系统,没有真正意义上的线程,它是用进程的 task_struct 和 数据结构 来模拟的线程,我们以前所学的进程,其实只是其内部只有一个执行流(主执行流)的特殊情况。其实,一个进程内部可以有多个执行流,Linux上所有的执行流统称为轻量级进程LWP ,也就是线程!!
4.Linux 线程的周边概念
a.线程的优点
①轻量化,由于线程所占据的资源更少,所以,线程的创建和销毁更加的轻量化。
②调度效率高,CPU对线程的调度要比对进程的调度更加高效(原因上文有过详细解释)。
**b.**线程的缺点
**①缺乏访问控制,**在一个进程中,由于线程会共享大部分资源,所以可能会有个别线程访问某些数据、函数会对进程整体造成影响,牵一发而动全身。
同时,编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了本不该共享的数据,而造成不良影响的可能性是很大的,且难以调试。
②同一进程内,但凡有一个线程因收到异常信号而被终止,整个进程内的所有线程都会被终止!!
c.线程的独立资源(同一进程内)
①*寄存器(就一组,存线程的上下文数据,独立的上下文能够保证线程是被独立调度的)
②*栈(保存线程在运行时形成的各种临时变量):独立的栈结构能够保证线程间运行不会出现执行流错乱问题。
③ID、errno、信号屏蔽字(block表)和调度优先级
每一个线程在运行时都要有自己独立的栈结构,因为每一个线程都会有自己的 调用链 , 栈结构会保存线程在运行时产生的临时变量,它存在的意义,本质就是为了支持我们在应用层管理" 整个调用链中临时变量空间的开辟和释放 "
d.线程的共享资源(同一进程内)
①*文件描述符表
②地址空间上的代码区、全局变量区、堆区、共享区等
③每种信号的处理方式(handler表)
④当前工作目录、用户id和组id
CPU调度线程时,对时间片怎么处理呢??--- 整个进程中的所有线程瓜分该进程的时间片,因为时间片也是资源。
二.线程控制
1.线程的创建
a.pthread_create ------ 原生线程库
①函数详解
int pthread_create(pthread_t* tid , const pthread_attr_t* attr , void*(*start_routine)(void*) , void* arg );
功能:创建一个新线程,并给新线程赋予任务。
参数一:tid 是由原生线程库自动生成的线程标识符,*tid是输出型参数。
参数二:*attr 也是输出型参数,用于获取线程的属性数据。
参数三:函数指针,void*()(void*),指向线程所执行的入口函数,函数参数需要我们自定义传入。
参数四:是给参数三(函数指针指向的函数参数)传递的实参。
返回值:若线程创建成功,返回0;否则返回错误码。
② pthread 线程库的底层原理
pthread_create() 库函数底层封装 的是 clone() 系统调用接口(轻量级进程的接口),而 clone() 函数会开辟一块空间给新创建的线程,充当新线程的栈!!
当调用了 pthread 库函数的可执行程序 加载到内存时,由于 pthread 是动态库,所以 pthread 库也要被加载到内存 ,并通过页表映射到地址空间共享区内!
至于线程ID是多少?栈空间多大?回调函数地址在哪?线程自己的时间片是多少?线程当前的状态?...... 等,一切都由 pthread库 维护、管理。
每一个 pthread 线程库(动态库,实质在内存上)级别的tcb(管理线程的结构体------线程控制块)的起始地址(在进程地址空间上),叫做线程的tid!!
主线程用程序地址空间里的栈,而轻量级进程创建的栈结构是在共享区内,具体来讲是在pthread库内对应线程的 tcb结构体 中。
b.thread ------ C++线程库
template <class Fn, class... Args> explicit thread (Fn&& fn, Args&&... args);
功能:创建一个新线程,并使新线程执行任务 fn,args 是函数 fn 的参数。
例如:
void increase_global (int n); 入口函数
thread new_thread = std::thread(increase_global,1000); 创建线程
注意:由于该构造函数被 expilcit 修饰,故 thread 不支持隐式类型转换。
2.线程的终止
a. return ------ 线程自然返回
当线程函数执行到return语句时,线程会自然结束其执行,并释放所占用的资源。
b. pthread_exit (void* retval)
pthread_exit() 函数是 POSIX 线程库中用于显式终止线程的函数,当线程调用 pthread_exit() 时,它会立即终止执行,并返回退出状态给主线程,该退出状态可以通过 pthread_join() 函数来获取。
c. pthread_cancel (tid)
pthread_cancel() 函数用于向目标线程发送取消请求,尝试终止该线程的执行 ,通常在主线程内调用 , 终止指定的线程。
d. 信号终止
线程也可以通过接收信号来终止执行。在Linux中,可以向线程发送信号,如SIGTERM(软件终止信号)。线程可以通过信号处理函数来响应这些信号,执行清理操作后退出。
e. 主线程退出
在Linux中,当主线程退出时,整个进程(包括所有线程)通常会随之终止。
终止线程的错误方法------exit(),任何一个线程调用exit(),都代表整个进程的退出!!
3.线程等待
a.什么是线程等待?
简单来讲,就是进行线程的资源回收,拿到线程的退出信息,以免造成资源泄露。
b.为什么要线程等待?
① 新线程被创建,主线程也要进行等待,若主线程不等待,也会造成类似僵尸进程问题。
② 为防止新线程内存泄漏,和获取新线程的退出信息,主线程要进行线程等待。
c.如何进行线程等待?
①**pthread_jion ------**原生线程库
int pthread_jion (pthread_t tid , void** retval);
功能:对目标线程进行线程等待,拿到该线程的退出信息,并对其进行资源释放。
retval是输出型参数 *retval 就是void*(*start_routine)(void*)函数(线程入口函数)的返回值
成功,返回0;失败,返回错误码。
当主线程执行到pthread_join时,若等待的目标线程仍未退出,则主线程阻塞等待!!
线程的参数和返回值,不仅仅可以用来进行传递一般参数,也可以传递对象!!
②join ------ C++11线程库
void join();
功能:将目标线程变成 non_joinable 状态,使其能够被安全释放。
注意:只有当目标进程退出后,才会执行 join() 函数,若否,则会阻塞住调用 join() 的执行流。
C++11中的线程库 ------ thread,其底层封装了Linux系统和Windows系统的原生线程库,使其具有跨平台性!!
三.多线程
1.多线程的创建
在一个循环中多次调用 pthread_create() 即可创建多个线程,如上图。创建出来的所有新线程都会进入 Entry() 函数。
其中每个线程的 Entry() 函数,都在线程自身的栈结构内,多线程间不共享各自栈结构内的数据。
线程之间没有秘密可言,同一进程内每个线程的所有数据都是可以被其他线程看到的,包括线程栈结构内的数据(如用一个全局的指针),只不过我们一般不会这么做。
全局变量是被所有的线程同时看到并访问的!
2.线程的局部存储
那么,如果我们想要使某一全局变量被线程私有 呢?
--- 线程的局部存储(即每个线程独立使用该全局变量),__thread int g_val = 100; 让全局变量被 "__thread"修饰即可,即线程的局部存储。
注意:__thread 是编译选项 ,只能修饰线程的局部存储且只能修饰内置类型数据,自定义类型不能用__thread 修饰!!
__thread,线程的局部存储有啥用??--- 它允许为每个线程分配独立的存储空间,从而避免了变量共享所带来的线程安全问题,因此不需要通过锁或其他同步机制来访问这些数据,从而减少了同步开销。
3.线程分离
默认情况下,新创建的线程是 joinable(需要被等待)的,线程退出后,需要对其进行线程等待,否则无法释放资源,从而造成系统内存泄漏。
但如果我们不关心线程的返回值,线程等待就成了一种负担,这个时候,我们可以告诉系统,当线程退出时,自动回收其资源即可!
获取线程ID:pthread_t pthread_self (void); //在线程内调用,获取线程自身的ID ------ tid
int pthread_detach (pthread_t tid);
****线程分离即可由主线程来做,亦可由该进程内其他线程来做,甚至线程可以自己分离自己,线程分离操作后,被分离的线程就不能再被 pthread_join 了,否则返回错误信息。
注意:当对某一线程设置 pthread_detach 后,若主线程先于该线程退出,那么这个线程的资源就会被立即释放!
4.线程互斥
当共享资源在被多线程并发访问时,就可能会出现这么一种情况,即:一个进程正在写入数据,而另一个进程正在读取数据,从而造成数据不一致问题!!
如:多个线程同时对一个全局变量进行--/++操作时,就可能导致该全局变量最终的结果与我们所预期的大相径庭。
为啥多线程对该全局变量进行操作时,屏幕上打印出来的数据很是杂乱?
*OS对tickets--操作的步骤:
1.现将内存里的tickets读入到CPU的寄存器中
2.CPU内部进行--运算
3.将计算结果写回到内存
所以,这就可能出现这么一种情况:A线程刚进入while()循环,就被OS从CPU上撤了下来,随后OS让CPU执行下一个线程B,当线程B对count进行多次--和打印操作后,OS再次进行线程间切换,运行线程A,当CPU恢复线程A上下文数据时,count的值又变成了线程B执行前的了,这就是导致线程B对count的操作全都变成了无用功!!
如何解决??--- 对共享数据的访问,保证任何时候只有一个执行流访问------互斥------锁
至于如何用锁来实习线程互斥,且听博主下文分解~~