【Linux学习笔记】线程概念和控制(二)
【Linux学习笔记】线程概念和控制(二)
🔥个人主页:大白的编程日记
🔥专栏:Linux学习笔记
文章目录
- 【Linux学习笔记】线程概念和控制(二)
- 前言
- 1.线程
- 1-1 线程的优点
- 1-2 线程的缺点
- 1-3 线程异常
- 1-4 线程用途
- 2. Linux进程VS线程
- 2-1 进程和线程
- 2-2 进程的多个线程共享
- 3. Linux线程控制
- 3-1 POSIX线程库
- 3-2 创建线程
- 3-3 线程终止
- pthread_exit函数
- pthread_cancel函数
- 3-4 线程等待
- 后言
前言
哈喽,各位小伙伴大家好!上期我们讲了线程概念和控制(一) 今天我们讲的是线程概念和控制(二)。话不多说,我们进入正题!向大厂冲锋!
1.线程
1-1 线程的优点
创建一个新线程的代价要比创建一个新进程小得多
与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
最主要的区别是线程的切换虚拟内存空间依然是相同的,但是进程切换是上下文切换的处理都是通过操作系统来完成的。内核的这种切换过程伴随的最显著的性能损耗是将寄存器中的内容切换出。
另外一个隐藏的损耗是上下文的切换会扰乱处理器的缓存机制。简单的说,一旦去切换上下文,处理器中所有已经缓存的内存地址一瞬间都作废了。还有一个显著的区别是你改变虚拟内存空间的时候,处理的页表缓冲 TLB(快表)会被全部刷新,这将导致内存的访问在一段时间内相当的低效。但是在线程的切换中,不会出现这个问题,当然还有硬件cache。
- 线程占用的资源要比进程少
- 能充分利用多处理器的可并行数量
- 在等待I/O操作结束的同时,程序可执行其他的计算任务
1-2 线程的缺点
- 性能损失
- 一个很少被外部事件阻塞的计算密集型线程往往无法与其他线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,实际的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
- 健壮性降低
- 编写线程需要更全面深入的考虑,在一个多线程程序里,因时间分配上的细微差异或者共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
- 缺乏访问控制
- 进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
- 编程难度提高
- 编写与调试一个多线程程序比单线程程序困难得多
- 编写与调试一个多线程程序比单线程程序困难得多
1-3 线程异常
单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃
线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出
1-4 线程用途
- 合理的使用多线程,能提高CPU密集型程序的执行效率
- 合理的使用多线程,能提高I/O密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)
2. Linux进程VS线程
进程间具有独立性
线程共享地址空间,也就共享进程资源
2-1 进程和线程
- 进程是资源分配的基本单位
- 线程是CPU调度的基本单位
线程共享进程数据,但也拥有自己的一部分“私有”数据:
- 线程ID
- 一组寄存器,线程的上下文数据
- 栈
- errno
- 信号屏蔽字
- 调度优先级
2-2 进程的多个线程共享
都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:
- 文件描述符表
- 各种信号的处理方式(SIG_IGN, SIG_DFL或者自定义的信号处理函数)
- 当前工作目录
- 用户id和组id
进程和线程的关系如下图:
3. Linux线程控制
3-1 POSIX线程库
与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头的
要使用这些函数,要通过引入头文件 <pthread.h>
。
链接这些线程函数时要使用编译器命令的 “-lpthread” 选项
3-2 创建线程
功能:创建一个新的线程
原型:
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变量的开销更小
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <pthread.h>void *rout(void *arg) {int i;for (; ; ) {printf("I am main thread\n");sleep(1);}
}int main( void ){int ret;pthread_t tid;if ( (ret=pthread_create(&tid, NULL, rout, NULL)) != 0 ){fprintf(stderr, "pthread_create : %s\n", strerror(ret));exit(EXIT_FAILURE);}int i;for (; ; ) {printf("I am main thread\n");sleep(1);}
}
打印出来的tid是通过pthread库中有函数 pthread_self
得到的,它返回一个 pthread_t 类型的变量,指代的是调用 pthread_self
函数的线程的“ID”。
怎么处理这个“ID”呢?这个“ID”是 pthread 库给每个线程定义的进程的唯一标识,是 pthread 库维护的。
由于每个进程有自己独立的内存空间,故此“ID”的作用域是进程级而非系统级(内核不认识)。
其实 pthread 库也是通过内核提供的系统调用(例如clone)来创建线程的,而内核会为每个线程的创建系统会全局唯一的“ID”来唯一标识这个线程。
使用PS命令查看线程信息
运行代码后执行:
$ ps -al | head -1 && ps -al | grep mythread
PID TTY TIME CMD
2711838 2711838 pts/235 00:00:00 mythread
2711838 2711839 pts/235 00:00:00 mythread
-L 选项:打印线程信息
LWP
是什么呢?
LWP 得到的是真正的线程ID。之前使用 pthread_self 得到的这个数字实际上是一个地址,在虚拟地址空间上的一个地址,通过这个地址,可以找到关于这个线程的基本信息,包括线程ID,线程栈,寄存器等属性。
在
ps -al
得到的线程ID,有一个线程ID和进程ID相同,这个线程就是主线程,主线程的栈在虚拟地址空间的栈上,而其他线程的栈是在共享区(堆栈之间),因为pthread系列函数都是pthread库提供给我们的。而pthread库是在共享区。所以除了主线程之外的线程的栈都在共享区。
3-3 线程终止
如果需要只终止某个线程而不终止整个进程,可以有三种方法:
- 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。
- 线程可以调用
pthread_exit
终止自己。 - 一个线程可以调用
pthread_cancel
终止同一进程中的另一个线程。
pthread_exit函数
功能:线程退出
原型:
void pthread_exit(void *value_ptr);
参数:
- value_ptr:value_ptr不要指向一个局部变量。
返回值:
- 无返回值,跟进程一样,线程结束的时候无法回到它的调用者(自身)
需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。
pthread_cancel函数
功能:取消一个执行中的线程
原型:
int pthread_cancel(pthread_t thread);
参数:thread:线程ID
返回值:成功返回0;失败返回错误码
3-4 线程等待
为什么需要线程等待?
- 已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。
- 创建新的线程不会复⽤刚才退出线程的地址空间。
功能:等待线程结束
原型
int pthread_join(pthread_t thread, void **value_ptr);
参数:
thread:线程ID
value_ptr:它指向⼀个指针,后者指向线程的返回值
返回值:成功返回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参数。
后言
这就是线程概念和控制(二)。大家自己好好消化!今天就分享到这! 感谢各位的耐心垂阅!咱们下期见!拜拜~