文章目录
- 线程的定义
- LINUX中的线程模型
- 一对一模型
- 多对一模型
- 多对多模型
- 线程实现原理
- 线程的状态
- 新建状态(New)
- 就绪状态(Runnable)
- 运行状态(Running)
- 阻塞状态(Blocked)
- 死亡状态(Dead)
- 线程的创建及使用
- 创建线程
- 线程的属性
- 相关函数
- 线程获取自身id
- 判断两个线程是否相等
- 判断两个线程是否相等有什么用??(待补充)
- 线程终止
- return 终止
- pthread_exit
- pthread_cancel
- pthread_exit 和 return的区别
- 线程的回收
- 线程的分离
- 线程的取消
- 什么是取消点
- 取消点的实现原理
- 相关函数
- int pthread_cancel(pthread_t thread)
- int pthread_setcancelstate(int state, int *oldstate)
- int pthread_setcanceltype(int type, int *oldtype)
- void pthread_testcancel(void)
- pthreads标准指定的取消点
- 线程的资源清理
- pthread_cleanup_push
- pthread_cleanup_pop
- 在pthread_exit时清理
- pthread_cancel时清理
- pthread_cleanup_pop的参数为非0值
- 不显式清理时,线程在什么时候清理资源
- 线程的同步
- 互斥锁实现线程同步
- 互斥锁的类型
- 普通锁(PTHREAD_MUTEX_NORMAL)
- 检错锁(PTHREAD_MUTEX_ERRORCHECK)
- 嵌套锁(PTHREAD_MUTEX_RECURSIVE)
- 默认锁(PTHREAD_MUTEX_ DEFAULT)
- 线程互斥锁的使用
- 自旋锁实现线程同步
- 自旋锁
- 自旋锁的使用
- 条件变量 + 互斥锁 实现同步
- 信号量实现同步
- 读写锁
- 线程的信号处理
- (补充)进程组与会话组
- (补充)用户线程如何映射到内核进程
线程的定义
线程(英语:thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。在Unix System V及SunOS中也被称为轻量进程(lightweight processes),但轻量进程更多指内核线程(kernel thread),而把用户线程(user thread)称为线程。
线程是独立调度和分派的基本单位。线程可以为操作系统内核调度的内核线程,如Win32线程;由用户进程自行调度的用户线程,如Linux平台的POSIX Thread;或者由内核与用户进程,如Windows 7的线程,进行混合调度。
同一进程中的多条线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。但同一进程中的多个线程有各自的调用栈(call stack),自己的寄存器环境(register context),自己的线程本地存储(thread-local storage)。
一个进程可以有很多线程,每条线程并行执行不同的任务。
在多核或多CPU,或支持Hyper-threading的CPU上使用多线程程序设计的好处是显而易见,即提高了程序的执行吞吐率。在单CPU单核的计算机上,使用多线程技术,也可以把进程中负责I/O处理、人机交互而常被阻塞的部分与密集计算的部分分开来执行,编写专门的workhorse线程执行密集计算,从而提高了程序的执行效率。
以上是百度给出的线程的定义,我们可以了解到:
1)线程是进程的一个分支,是一个单一的顺序流程
2)一个进程最少拥有一个线程==>主线程即程序本身
3)同进程的多个线程共享进程的的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。
4)每个线程拥有自己的调用栈(call stack),自己的寄存器环境(register context),自己的线程本地存储(thread-local storage)。
LINUX中的线程模型
linux 最开始使用的线程是linuxThreads, 但是linuxThreads不符合POSIX标准, 后来出现了NGPT, 性能更高, 之后又出现了NPTL, 比NGPT更快, 随着时间推移, 就只剩下NPTL了。
线程的模型分为三种:
多对一(M:1)的用户级线程模型
一对一(1:1)的内核级线程模型: 例如linuxThreads和NPTL
多对多(M:N)的两极线程模型: 例如NGPT
查看当前系统使用的进程库
getconf GNU_LIBPTHREAD_VERSION
wangju@wangju-virtual-machine:/usr/include$ getconf GNU_LIBPTHREAD_VERSION
NPTL 2.31
wangju@wangju-virtual-machine:/usr/include$ ^C
Ubuntu使用NPTL线程库,NPTL 是 LinuxThreads 的替代者,而且其符合了 POSIX 的标准,在稳定性和性能方面都有了很大的提升。和 LinuxThreads 一样,NPTL 采用了一对一的线程模型。
一对一模型
一个用户线程对应一个内核线程。内核负责每个线程的调度,可以调度到其他处理器上面。
优点:
实现简单。
缺点:
对用户线程的大部分操作都会映射到内核线程上,引起用户态和内核态的频繁切换。
内核为每个线程都映射调度实体,如果系统出现大量线程,会对系统性能有影响。
多对一模型
顾名思义,多对一线程模型中,多个用户线程对应到同一个内核线程上,线程的创建、调度、同步的所有细节全部由进程的用户空间线程库来处理。
优点:
用户线程的很多操作对内核来说都是透明的,不需要用户态和内核态的频繁切换。使线程的创建、调度、同步等非常快。
缺点:
由于多个用户线程对应到同一个内核线程,如果其中一个用户线程阻塞,那么该其他用户线程也无法执行。
内核并不知道用户态有哪些线程,无法像内核线程一样实现较完整的调度、优先级等。
多对多模型
多对一线程模型是非常轻量的,问题在于多个用户线程对应到固定的一个内核线程。多对多线程模型解决了这一问题:m个用户线程对应到n个内核线程上,通常m>n。由IBM主导的NGPT采用了多对多的线程模型,不过现在已废弃。
优点:
兼具多对一模型的轻量
由于对应了多个内核线程,则一个用户线程阻塞时,其他用户线程仍然可以执行
由于对应了多个内核线程,则可以实现较完整的调度、优先级等
缺点:
实现复杂
线程实现原理
首先明确进程与进程的基本概念:
1)进程是资源分配的基本单位
2)线程是CPU调度的基本单位
3)一个进程下可能有多个线程
4)线程共享进程的资源
linux用户态的进程、线程基本满足上述概念,但内核态不区分进程和线程。可以认为,内核中统一执行的是进程,但有些是“普通进程”(对应进程process),有些是“轻量级进程”(对应线程pthread或npthread),都使用task_struct结构体保存保存。使用fork创建进程,使用pthread_create创建线程。两个系统调用最终都都调用了do_dork,而do_dork完成了task_struct结构体的复制,并将新的进程加入内核调度。
普通进程需要深拷贝虚拟内存、文件描述符、信号处理等;而轻量级进程之所以“轻量”,是因为其只需要浅拷贝虚拟内存等大部分信息,多个轻量级进程共享一个进程的资源。
linux加入了线程组的概念,让原有“进程”对应线程,“线程组”对应进程,实现“一个进程下可能有多个线程”。
task_struct中,使用pgid标的进程组,tgid标的线程组,pid标的进程或线程。
一个进程组包含多个进程,一个进程包含一个线程组,一个线程组包含多个线程,所以tgid相同的task_struct属于同一个线程组,即属于同一个进程(pid == tgid)。
在一个线程组内 线程id pid=tgid(线程组号)的是主线程,其余线程地位相等。
因此,调用getpgid返回pgid(主进程号),调用getpid应返回tgid(主线程号=进程号),调用gettid应返回pid(线程号)。
进程下除主线程外的其他线程是CPU调度的基本单位,这很好理解。而所谓主线程与所属进程实际上是同一个task_struct,也能被CPU调度,因此主线程也是CPU调度的基本单位。tgid相同的所有线程组成了概念上的“进程”,只有主线程在创建时会实际分配资源,其他线程通过浅拷贝共享主线程的资源。结合前面介绍的普通线程与轻量级进程,实现“进程是资源分配的基本单位”。
进程是一个逻辑上的概念,用于管理资源,对应task_struct中的资源,每个进程至少有一个线程,用于具体的执行,对应task_struct中的任务调度信息,以task_struct中的pid区分线程,tgid区分进程,pgid区分进程组
线程的状态
新建状态(New)
线程被创建后,但还没有调用start()方法。
就绪状态(Runnable)
调用start()方法后,线程处于就绪状态,等待CPU调度。
运行状态(Running)
线程获取CPU资源执行。
阻塞状态(Blocked)
线程因为某些原因放弃CPU使用权,暂时停止运行。
死亡状态(Dead)
线程执行完毕或被终止后的状态
线程的创建及使用
创建线程
#include <pthread.h>
typedef unsigned long int pthread_t;
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
参数:
pthread_t * :成功创建时接收线程id
pthread_attr_t :设置线程属性,一般为NULL。
void *(*start_routine) (void *):返回值为void参数为void的函数,线程的执行函数。
void *arg:给线程函数传参。
返回值:
成功:返回0;
失败:返回错误号 errno,并且第一个参数不会被设置
线程的属性
创建线程时通过参数pthread_attr_t 设置线程的属性
union pthread_attr_t
{
char __size[__SIZEOF_PTHREAD_ATTR_T];
long int __align;
};
#ifndef __have_pthread_attr_t
typedef union pthread_attr_t pthread_attr_t;
# define __have_pthread_attr_t 1
typedef struct
{
int detachstate; // 线程的分离状态
int schedpolicy; // 线程调度策略
structsched_param schedparam; // 线程的调度参数
int inheritsched; // 线程的继承性
int scope; // 线程的作用域
size_t guardsize; // 线程栈末尾的警戒缓冲区大小
int stackaddr_set; // 线程的栈设置
void* stackaddr; // 线程栈的位置
size_t stacksize; // 线程栈的大小
} pthread_attr_t;
Posix线程中的线程属性pthread_attr_t主要包括scope属性、detach属性、堆栈地址、堆栈大小、优先级。在pthread_create中,把第二个参数设置为NULL的话,将采用默认的属性配置。
线程具有属性,用pthread_attr_t表示,在对该结构进行处理之前必须进行初始化,在使用后需要对其去除初始化。
调用pthread_attr_init之后,pthread_t结构所包含的内容就是操作系统实现支持的线程所有属性的默认值。
如果要去除对pthread_attr_t结构的初始化,可以调用pthread_attr_destroy函数。如果pthread_attr_init实现时为属性对象分配了动态内存空间,pthread_attr_destroy还会用无效的值初始化属性对象,因此如果经pthread_attr_destroy去除初始化之后的pthread_attr_t结构被pthread_create函数调用,将会导致其返回错误。
相关函数
- 1、pthread_attr_init
功能: 对线程属性变量的初始化。 - 2、pthread_attr_setscope
功能: 设置线程 __scope 属性。scope属性表示线程间竞争CPU的范围,也就是说线程优先级的有效范围。POSIX的标准中定义了两个值:PTHREAD_SCOPE_SYSTEM和PTHREAD_SCOPE_PROCESS,前者表示与系统中所有线程一起竞争CPU时间,后者表示仅与同进程中的线程竞争CPU。默认为PTHREAD_SCOPE_PROCESS。目前LinuxThreads仅实现了PTHREAD_SCOPE_SYSTEM一值。 - 3、pthread_attr_setdetachstate
功能: 设置线程detachstate属性。该表示新线程是否与进程中其他线程脱离同步,如果设置为PTHREAD_CREATE_DETACHED则新线程不能用pthread_join()来同步,且在退出时自行释放所占用的资源。缺省为PTHREAD_CREATE_JOINABLE状态。这个属性也可以在线程创建并运行以后用pthread_detach()来设置,而一旦设置为PTHREAD_CREATE_DETACH状态(不论是创建时设置还是运行时设置)则不能再恢复到PTHREAD_CREATE_JOINABLE状态。 - 4、pthread_attr_setschedparam
功能: 设置线程schedparam属性,即调用的优先级。 - 5、pthread_attr_getschedparam
功能: 得到线程优先级。
原文链接:https://blog.csdn.net/houzijushi/article/details/80978345
线程获取自身id
#include <pthread.h>
pthread_t pthread_self(void);
返回值:返回线程号,即对应task_struct 的 pid,并且该函数总是会成功
判断两个线程是否相等
int pthread_equal(pthread_t t1, pthread_t t2);
作用:比较两个线程ID是否相等。
返回值:相等返回非0值,不等返回0值。
判断两个线程是否相等有什么用??(待补充)
线程终止
线程终止有三种方式
1)return
2)pthread_exit
3) pthread_cancel
默认属性的线程执行结束后并不会立即释放占用的资源,直到整个进程执行结束,所有线程的资源以及整个进程占用的资源才会被操作系统回收。实现线程资源及时回收的常用方法有两种,一种是修改线程属性,另一种是在另一个线程中调用 pthread_join() 函数。如果主线程提前结束,会终止所有的同组线程。
return 终止
由上面的分析,我们了解到线程执行的是一个指定函数的内容,所以该函数return之后函数运行结束,调用它的线程也终止。return的值也可以被pthread_join函数接收。
pthread_exit
#include <pthread.h>
void pthread_exit(void *retval);
参数:
retval 是 void* 类型的指针,可以指向任何类型的数据,它指向的数据将作为线程退出时的返回值。如果线程不需要返回任何数据,将 retval 参数置为 NULL 即可。
注意,retval 指针不能指向函数内部的局部数据(比如局部变量)。换句话说,pthread_exit() 函数不能返回一个指向局部数据的指针,否则很可能使程序运行结果出错甚至崩溃。
pthread_cancel
#include <pthread.h>
int pthread_cancel(pthread_t thread);
参数 :thread 指定需要取消的目标线程;成功返回 0,失败将返回错误码。
通过调用 pthread_cancel()库函数向一个指定的线程发送取消请求,要求指定线程终止。可以在一个线程中取消另一个线程。
*发出取消请求之后,函数 pthread_cancel()立即返回,不会等待目标线程的退出。默认情况下,目标线程也会立刻退出,其行为表现为如同调用了参数为 PTHREAD_CANCELED(其实就是(void )-1)的pthread_exit()函数,但是,线程可以设置自己不被取消或者控制如何被取消,所以 pthread_cancel()并不会等待线程终止,仅仅只是提出请求。
pthread_exit 和 return的区别
无论是采用 return 语句还是调用 pthread_exit() 函数,主线程中的 pthread_join() 函数都可以接收到线程的返回值。return 语句和 pthread_exit() 函数的含义不同,return 的含义是返回,它不仅可以用于线程执行的函数,普通函数也可以使用;pthread_exit() 函数的含义是线程退出,它专门用于结束某个线程的执行。
此外,pthread_exit() 可以自动调用线程清理程序(本质是一个由 pthread_cleanup_push() 指定的自定义函数),return 则不具备这个能力。总之在实际场景中,如果想终止某个子线程执行,强烈建议大家使用 pthread_exit() 函数。终止主线程时,return 和 pthread_exit() 函数发挥的功能不同,可以根据需要自行选择。
线程的回收
int pthread_join(pthread_t thread, void **retval);
参数:
thread:要等待回收的线程号
retval:接收return或pthread_exit返回的参数
return:成功返回0,失败返回errno号。
当指定的线程已经终止时立即返回,否则阻塞等待。
使用pthread_join时线程必须是未分离的,否则不可用
当调用 pthread_join() 时,当前线程会处于阻塞状态,直到被调用的线程结束后,当前线程才会重新开始执行。当 pthread_join() 函数返回后,被调用线程才算真正意义上的结束,它的内存空间也会被释放(如果被调用线程是非分离的)。这里有三点需要注意:
- 被释放的内存空间仅仅是系统空间,你必须手动清除程序分配的空间,比如 malloc() 分配的空间。
- 一个线程只能被一个线程所连接(阻塞等待)。
- 被连接的线程必须是非分离的,否则连接会出错。
所以可以看出pthread_join()有两种作用:
1)用于等待其他线程结束:当调用 pthread_join() 时,当前线程会处于阻塞状态,直到被调用的线程结束后,当前线程才会重新开始执行。
2)对线程的资源进行回收:如果一个线程是非分离的(默认情况下创建的线程都是非分离)并且没有对该线程使用 pthread_join() 的话,该线程结束后并不会释放其内存空间,这会导致该线程变成了“僵尸线程”。
原文链接:https://blog.csdn.net/yzy1103203312/article/details/80849831
线程的分离
int pthread_detach(pthread_t thread);
将指定线程与指控线程分离,线程结束后(不会产生僵尸线程),其退出状态不由其他线程获取,而直接自己自动释放(自己清理掉PCB的残留资源)。
一般情况下,线程终止后,其终止状态一直保留到其它线程调用pthread_join获取它的状态为止(或者进程终止被回收了)。但是线程也可以被置为detach状态,这样的线程一旦终止就立刻回收它占用的所有资源,而不保留终止状态。不能对一个已经处于detach状态的线程调用pthread_join,这样的调用将返回EINVAL错误。
线程的取消
什么是取消点
1)pthread_cancel调用并不等待线程终止,它只提出请求。线程在取消请求(pthread_cancel)发出后会继续运行,直到到达某个取消点(CancellationPoint)。取消点是线程检查是否被取消并按照请求进行动作的一个位置,即取消点是线程判断是否可被取消的位置,程序产生了取消点,则可以被取消。
2)线程取消的方法是向目标线程发Cancel信号,但如何处理Cancel信号则由目标线程自己决定,或者忽略、或者立即终止、或者继续运行至Cancelation-point(取消点),由不同的Cancelation状态决定。
3)线程的取消点,也称为“中断点”或“cancel point”,是在程序设计中设定的一个特殊位置,其目的是允许外部请求暂停或者停止正在运行的线程。当线程到达这个点时,它会检查是否有被中断的情况发生,如果有,就会执行相应的中断处理逻辑。
作用主要有三个:
①响应中断:线程可能会在一个操作完成后检查是否需要中断,如网络请求、长时间计算等,以便及时响应中断信号,避免资源浪费。
②控制流程:通过设置取消点,程序员可以更好地管理复杂的异步任务,例如,在等待某个条件满足时,如果需要提前结束,可以在取消点上让线程退出。
③异常处理:作为一种优雅的方式,线程的取消点可以帮助捕获并处理非预期的终止请求,比如用户关闭应用程序时,主线程可以设置一个取消点来清理资源或记录日志。
取消点总结:取消点是线程stat=PTHREAD_CANCEL_ENABLE,type=PTHREAD_CANCEL_DEFFERED时,线程内部相应Cancel信号的位置。stat=PTHREAD_CANCEL_ENABLE,type=PTHREAD_CANCEL_ASYCHRONOUS时,立即响应Cancel信号,与取消点无关。
取消点的实现原理
下面我们看 Linux 是如何实现取消点的。(其实这个准确点儿应该说是 GNU 取消点实现,因为 pthread 库是实现在 glibc 中的。) 我们现在在 Linux 下使用的 pthread 库其实被替换成了 NPTL,被包含在 glibc 库中。以 pthread_cond_wait 为例,glibc-2.6/nptl/pthread_cond_wait.c 中:
/* Enable asynchronous cancellation. Required by the standard. */
cbuffer.oldtype = __pthread_enable_asynccancel ();
/* Wait until woken by signal or broadcast. */
lll_futex_wait (&cond->__data.__futex, futex_val);
/* Disable asynchronous cancellation. */
__pthread_disable_asynccancel (cbuffer.oldtype);
我们可以看到,在线程进入等待之前,pthread_cond_wait 先将线程取消类型设置为异步取消(__pthread_enable_asynccancel),当线程被唤醒时,线程取消类型被修改回延迟取消 __pthread_disable_asynccancel 。
这就意味着,所有在 __pthread_enable_asynccancel 之前接收到的取消请求都会等待 __pthread_enable_asynccancel 执行之后进行处理,所有在 __pthread_disable_asynccancel 之前接收到的请求都会在 __pthread_disable_asynccancel 之前被处理,所以真正的 Cancellation Point 是在这两点之间的一段时间。
原文链接 https://blog.csdn.net/m0_46535940/article/details/124908464
相关函数
int pthread_cancel(pthread_t thread)
发送终止信号给thread线程,如果成功则返回0,否则为非0值。发送成功并不意味着thread会终止。
int pthread_setcancelstate(int state, int *oldstate)
设置本线程对Cancel信号的反应**(动作),state有两种值:PTHREAD_CANCEL_ENABLE(缺省)和PTHREAD_CANCEL_DISABLE,分别表示收到信号后设为CANCLED状态和忽略CANCEL信号**继续运行;old_state如果不为NULL则存入原来的Cancel状态以便恢复。
int pthread_setcanceltype(int type, int *oldtype)
设置本线程取消动作的执行时机**(类型),type由两种取值:PTHREAD_CANCEL_DEFFERED和PTHREAD_CANCEL_ASYCHRONOUS,仅当Cancel状态为Enable时有效,分别表示收到信号后继续运行至下一个取消点再退出和立即执行取消动作(退出)**;oldtype如果不为NULL则存入运来的取消动作类型值。
void pthread_testcancel(void)
是说pthread_testcancel在不包含取消点,但是又需要取消点的地方创建一个取消点,以便在一个没有包含取消点的执行代码线程中响应取消请求.线程取消功能处于启用状态且取消状态设置为延迟状态时(表示 stat=PTHREAD_CANCEL_ENABLE type=PTHREAD_CANCEL_DEFFERED),pthread_testcancel()函数有效。如果在取消功能处处于禁用状态下调用pthread_testcancel(),则该函数不起作用。请务必仅在线程取消线程操作安全的序列中插入pthread_testcancel()。除通pthread_testcancel()调用以编程方式建立的取消点意外,pthread标准还指定了几个取消点。测试退出点,就是测试cancel信号。
pthreads标准指定的取消点
(1)通过pthread_testcancel调用以编程方式建立线程取消点。
(2)线程等待pthread_cond_wait或pthread_cond_timewait()中的特定条件。
(3)被sigwait(2)阻塞的函数
(4)一些标准的库调用。通常,这些调用包括线程可基于阻塞的函数。
线程的资源清理
主线程可以通道 pthread_cancel 主动终止子线程,但是子线程中可能还有未被释放的资源,比如malloc开辟的空间。如果不清理,很有可能会造成内存泄漏。因此,为了避免这种情况,于是就有了一对线程清理函数 pthread_cleanup_push 和 pthread_cleanup_pop 。两者必须是成对存在的,否则无法编译通过。
pthread_cleanup_push 和 pthread_cleanup_pop都是宏定义
# define pthread_cleanup_push(routine, arg) \
do { \
__pthread_cleanup_class __clframe (routine, arg)
/* Remove a cleanup handler installed by the matching pthread_cleanup_push.
If EXECUTE is non-zero, the handler function is called. */
# define pthread_cleanup_pop(execute) \
__clframe.__setdoit (execute); \
} while (0)
可以看到只有成对出现时代码才是完整的push { , pop }。
pthread_cleanup_push
void pthread_cleanup_push(void (*routine)(void *), void *arg);
第一个参数 routine:回调清理函数。当上面三种情况的任意一种存在时,回调函数就会被调用
第二个参数 args:要传递个回调函数的参数
pthread_cleanup_push 的作用是创建栈帧,设置回调函数,该过程相当于入栈。回调函数的执行与否有以下三种情况:
线程被取消的时候(pthread_cancel)
线程主动退出的时候(pthread_exit)
pthread_cleanup_pop的参数为非0值(pthread_cleanup_pop)
pthread_cleanup_pop
void pthread_cleanup_pop(int execute);
当 execute = 0 时, 处在栈顶的栈帧会被销毁,pthread_cleanup_push的回调函数不会被执行
当 execute != 0 时,pthread_cleanup_push 的回调函数会被执行。
pthread_cleanup_pop 函数的作用是执行回调函数 或者 销毁栈帧,该过程相当于出栈。根据传入参数的不同执行的结果也会不同。
在pthread_exit时清理
这里 pthread_cleanup_pop 函数的放置位置和参数需要注意:
必须放在 pthread_exit 后面,否则pthread_cleanup_pop会先清除栈帧,pthread_exit就无法调用清理函数了。pthread_cleanup_pop的参数是 0,因为pthread_cleanup_pop的参数为非0值时也会调用回调清理函数
void* pthread_cleanup(void* args){
printf("线程清理函数被调用了\n");
}
void* pthread_run(void* args)
{
pthread_cleanup_push(pthread_cleanup, NULL);
pthread_exit((void*)1); // 子线程主动退出
pthread_cleanup_pop(0); // 这里的参数要为0,否则回调函数会被重复调用
}
int main(){
pthread_t tid;
pthread_create(&tid, NULL, pthread_run, NULL);
sleep(1);
pthread_join(tid, NULL);
return 0;
}
pthread_cancel时清理
这里 pthread_cleanup_pop 函数的放置位置和参数需要注意,放置在取消点后,pop参数为0
void* pthread_cleanup(void* args){
printf("线程清理函数被调用了\n");
}
void* pthread_run(void* args)
{
pthread_cleanup_push(pthread_cleanup, NULL);
pthread_testcancel(); // 设置取消点
pthread_cleanup_pop(0); // 这里的参数要为0,否则回调函数会被重复调用
}
int main(){
pthread_t tid;
pthread_create(&tid, NULL, pthread_run, NULL);
pthread_cancel(tid); // 取消线程
sleep(1);
pthread_join(tid, NULL);
return 0;
}
pthread_cleanup_pop的参数为非0值
void* pthread_cleanup(void* args){
printf("线程清理函数被调用了\n");
}
void* pthread_run(void* args)
{
pthread_cleanup_push(pthread_cleanup, NULL);
pthread_cleanup_pop(1); // 这里的参数为非0值
}
int main(){
pthread_t tid;
pthread_create(&tid, NULL, pthread_run, NULL);
sleep(1);
pthread_join(tid, NULL);
return 0;
}
不显式清理时,线程在什么时候清理资源
在C语言中,使用pthread库进行并发处理时,pthread_cleanup_push和pthread_cleanup_pop函数是用来管理清理上下文(cleanup context)的,它们主要用于指定在退出某个作用域时需要执行的清理操作。如果你没有调用pthread_cleanup_pop来弹出并执行之前设置的清理操作,那么这些清理任务通常会在以下几个情况下自动发生:
线程结束:当线程因为正常退出(如调用pthread_exit或其终止信号导致),系统会自动执行清理上下文中列出的任务。
异常退出:如果线程由于未捕获的错误或异常而被迫终止,清理操作将在内核层执行,尽管这可能会因平台而异。
栈溢出:如果线程的堆栈空间不足以完成当前的操作,可能导致栈溢出,这时清理可能不会被执行,取决于系统的行为。
但是,如果没有显式地通过pthread_cleanup_pop来控制清理顺序,不保证所有注册的清理操作一定会按预期执行,尤其是当线程提前中断时。因此,推荐在合适的位置调用pthread_cleanup_pop来管理和控制清理行为。如果不希望在特定条件下执行清理,可以手动清除清理队列,例如使用pthread_cleanup_destroy。
线程的同步
互斥锁实现线程同步
互斥锁的类型
互斥锁本质就是一个特殊的全局变量,拥有lock和unlock两种状态,unlock的互斥锁可以由某个线程获得,当互斥锁由某个线程持有后,这个互斥锁会锁上变成lock状态,此后只有该线程有权力打开该锁,其他想要获得该互斥锁的线程都会阻塞,直到互斥锁被解锁。
普通锁(PTHREAD_MUTEX_NORMAL)
互斥锁默认类型。当一个线程对一个普通锁加锁以后,其余请求该锁的线程将形成一个 等待队列,并在该锁解锁后按照优先级获得它,这种锁类型保证了资源分配的公平性。一个 线程如果对一个已经加锁的普通锁再次加锁,将引发死锁;对一个已经被其他线程加锁的普 通锁解锁,或者对一个已经解锁的普通锁再次解锁,将导致不可预期的后果。
检错锁(PTHREAD_MUTEX_ERRORCHECK)
一个线程如果对一个已经加锁的检错锁再次加锁,则加锁操作返回EDEADLK;对一个已 经被其他线程加锁的检错锁解锁或者对一个已经解锁的检错锁再次解锁,则解锁操作返回 EPERM。
嵌套锁(PTHREAD_MUTEX_RECURSIVE)
该锁允许一个线程在释放锁之前多次对它加锁而不发生死锁;其他线程要获得这个锁,则当前锁的拥有者必须执行多次解锁操作;对一个已经被其他线程加锁的嵌套锁解锁,或者对一个已经解锁的嵌套锁再次解锁,则解锁操作返回EPERM。
默认锁(PTHREAD_MUTEX_ DEFAULT)
一个线程如果对一个已经加锁的默认锁再次加锁,或者虽一个已经被其他线程加锁的默 认锁解锁,或者对一个解锁的默认锁解锁,将导致不可预期的后果;这种锁实现的时候可能 被映射成上述三种锁之一。
线程互斥锁的使用
// 静态方式创建互斥锁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
// 动态方式创建互斥锁,其中参数mutexattr用于指定互斥锁的类型,具体类型见上面四种,如果为NULL,就是普通锁。
int pthread_mutex_init (pthread_mutex_t* mutex,const pthread_mutexattr_t* mutexattr);
int pthread_mutex_lock(pthread_mutex_t *mutex); // 加锁,阻塞
int pthread_mutex_trylock(pthread_mutex_t *mutex); // 尝试加锁,非阻塞
int pthread_mutex_unlock(pthread_mutex_t *mutex); // 解锁
自旋锁实现线程同步
自旋锁
自旋锁顾名思义就是一个死循环,不停的轮询,当一个线程未获得自旋锁时,不会像互斥锁一样进入阻塞休眠状态,而是不停的轮询获取锁,如果自旋锁能够很快被释放,那么性能就会很高,如果自旋锁长时间不能够被释放,甚至里面还有大量的IO阻塞,就会导致其他获取锁的线程一直空轮询,导致CPU使用率达到100%,特别CPU时间。
自旋锁的使用
int pthread_spin_init(pthread_spinlock_t *lock, int pshared); // 创建自旋锁
int pthread_spin_lock(pthread_spinlock_t *lock); // 加锁,阻塞
int pthread_spin_trylock(pthread_spinlock_t *lock); // 尝试加锁,非阻塞
int pthread_spin_unlock(pthread_spinlock_t *lock); // 解锁
条件变量 + 互斥锁 实现同步
Linux 条件变量:实现线程同步(什么是条件变量、为什么需要条件变量,怎么使用条件变量(接口)、例子,代码演示(生产者消费者模型))
信号量实现同步
使用系统的信号量实现同步。
linux线程同步方式3——信号量(semaphore)
读写锁
linux线程同步方式5——读写锁(rwlock)
线程的信号处理
Linux下多线程中的信号处理