前言
线程是进程当中的一条执行流,执行一个程序中的部分代码,运行在进程的虚拟地址空间内部,共享进程的大部分资源,线程也被称为轻量级进程。
进程:是系统资源分配的基本单位(操作系统会为运行中的程序分配所需要的资源)
线程:是cpu调度的基本单位(操作系统是通过pcb实现程序的调度和运行)
一:线程间的独有与共享
1.1 线程间独有
线程ID: 线程ID在进程中是唯一的,进程用线程ID来标识线程。
寄存器: 线程间是并发运行的,每个线程都有自己不同的运行线索(上下文数据、程序计数器),独立寄存器保存线程运行状态,便于线程的准确调度切换。
栈: 线程调用函数有可能是层层嵌套,每个线程都有自己独立的栈,防止调用栈混乱。
信号屏蔽字: 每个线程都有自己独立的信号屏蔽字,阻塞信号,交给其他线程处理。
错误返回码: errno是一个全局变量,多个线程并行,必须保证errno独有,否则errno有可能被覆盖产生歧义。
调度优先级: 确定线程的调度优先次序。
1.2 线程间共享
虚拟地址空间: 共享代码段和数据段,全局变量通信。
文件描述符表: 指向同一个文件,各个线程都能文件。
信号处理器: 信号针对进程产生效果,线程共享信号处理方式,进程才能准确处理信号。
工作路径: 进程共享工作路径,用户ID,组ID。
二:多任务处理
多任务处理有两种形式:多线程任务处理和多进程任务处理。
2.1 多线程任务处理
线程间通信更加灵活方便:
共用同一块虚拟地址空间,进程间通信方式以及全局变量或函数传参的方式都能实现线程间通信。
线程的创建与销毁的成本更低:
创建线程不需要从新创建虚拟地址空间和页表,共用的资源只需要增加一个指针指向即可。
线程间的调度成本更低:
线程的执行力度更细致,虚拟地址空间、页表、IO信息共用,调度切换的成本更低。
2.2 多进程任务处理
进程间更为稳定健壮:
有些异常以及系统调用针对整个进程生效,进程间独立性较强,多进程更为稳定健壮。
2.3 共同优点
IO密集型:大量的IO操作(同时发起文件IO,缩短等待时间)
CPU密集型:大量的数据运算处理(并行运算,提高效率)
并行压缩CPU处理时间和IO等待的时间
执行流不是越多越好,多了反而会增加调度切换的成本,通常执行流个数是CPU核心个数+1(防止执行流阻塞),这样每个执行流都会占用一个CPU核心进行处理,充分利用CPU。
三:线程控制
线程控制的接口都是库函数(操作系统并没有提供线程的创建接口)
3.1 线程创建
线程的创建就是在内核中创建一个pcb,调度完成线程入口函数。
int pthread_creat (pthread_t✳ tid,const pthread_attr_t✳ attr,void✳(✳start_routine)(void✳),void✳ arg)
- tid:
用于获取线程id(线程的操作句柄),可以找到线程的描述信息,访问pcb完成控制,操作线程。
tid:用户态线程id,线程的操作句柄(线程独有的这块首地址),每个线程创建之后,都会开辟一块空间存储自己的栈和描述信息(线程地址空间)
pid:轻量级进程id,存在于内核task_struct结构体(ps -efL看到的LWP)
tgid:线程组id,是主线程id,也就是外面看到的进程id。
- attr:
线程属性(通常置空)
一般设置线程属性用于直接对线程生效的接口。栈大小、joinable属性、detach属性、阻塞信号的继承属性。
- start_routine:
线程入口函数,创建一个线程就是为了运行这个函数,函数运行完毕,则线程退出。
- arg:
通过线程入口函数,给线程传递的参数
- 返回值:
成功返回0,失败返回非零值errno。
3.2 线程终止
退出一个线程
线程入口函数中的return
注意:main中return退出的是进程
pthread_exit(void✳ retval)
退出调用线程,主线程退出,进程并不会退出,所有线程退出,进程则退出。
pthread_cancel(pthread_t tid)
发送一个取消请求到线程(被动取消)
3.3 线程等待
一个线程等待另一个线程的退出,获取退出线程的返回值,并且回收退出线程的资源(每个线程都有自己独立的线程地址空间)
线程被等待的前提: 这个线程处于joinable属性(不会自动回收资源,需要被等待)
pthread_join(pthread_t tid,void✳✳ retval) (阻塞等待)
retval:输出型参数(返回线程的返回值)
3.4 线程分离
线程被分离的前题: 不关心线程的处理结果
pthread_detach(pthread_t tid)
pthread_t pthread_self(void)
将线程的joinable属性设置为detach属性(退出自动回收资源),被分离的线程因为资源自动被释放,因此不需要等待。
四:线程安全
线程间通信极为方便灵活,但是多个线程作为多个执行流对同一临界资源进行争抢访问,有可能会出现数据二义性。
线程安全:多个线程对临界资源进行访问操作而不会造成数据的二义性。
4.1 线程安全的实现
同步与互斥:
同步:通过条件判断保证对临界资源访问的时序合理性
互斥:通过同一时间唯一访问保证对临界资源访问的安全性
同步的实现(并不保证安全)
若当前线程不能对临界资源进行访问,则让线程等待,等到临界资源满足被线程访问的条件后,这时候唤醒线程的等待。
条件变量:
向外提供接口实现等待与唤醒的功能,访问条件是否满足由自己完成。
1.定义条件变量:
pthread_cond_t cond
2.初始化条件变量:
pthread_cond_init(pthread_cond_t * ,pthread_condaddr_t * )
3.使线程挂起休眠:
pthread_cond_wait(pthread_cond_t * ,pthread_mutex_t * )
pthread_cond_timedwait(pthread_cond_t * ,pthread_mutex_t * ,struct timespec)
4.唤醒线程:
pthread_cond_signal(pthread_cond_t * )唤醒至少一个线程
pthread_cond_broadcast(pthread_cond_t * )唤醒全部等待线程
5.销毁条件变量:
pthread_cond_destroy(pthread_cond_t * )
互斥的实现(并不保证合理)
互斥锁:
通过只有0或1的计数器描述了一个临界资源当前的访问状态,所有执行流在访问临界资源之前都需要判断临界资源是否可访问,如果不可访问则让执行流等待,可访问则进行访问,在访问期间将临界资源置为不可访问状态,使其他执行流不可访问该临界资源。
1.定义互斥锁变量:
pthread_mutex_t mutex
2.初始化互斥锁变量:
pthread_mutex_init(pthread_mutex_t* mutex,NULL)
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
3.在访问临界资源之前进行加锁操作:
pthread_mutex_lock(pthread_mutex_t* mutex)阻塞加锁
pthread_mutex_trylock(pthread_mutex_t* mutex)非阻塞加锁
不能加锁则等待:将线程状态置为可中断休眠状态
能加锁则加锁:修改临界资源访问状态进行临界资源的访问
4.在临界资源访问完毕之后进行解锁操作:
将临界资源状态置为可访问并唤醒等待线程
pthread_mutex_unlock(pthread_mutex_t* mutex)
5.销毁互斥锁:
pthread_mutex_destroy(pthread_mutex_t* mutex)
互斥锁实现自身原子操作的原理
将寄存器中的值置为0(不可访问),再将寄存器中的值与内存中的值进行交换,这个交换过程是一步完成的,这时内存中临界资源就为不可访问状态,其它线程无法加锁,保证操作的原子性。
本文介绍了线程的概念,包括线程与进程的区别、线程间的资源共享与独立性,以及多任务处理方式。同时深入探讨了线程控制,如创建、终止、等待和分离等操作,并详细讲解了线程安全问题及其实现方法,如使用互斥锁和条件变量确保数据一致性。
1970

被折叠的 条评论
为什么被折叠?



