1 知识简介
1.1 概述
取消一个线程要确保该线程能够释放其所持有的任何锁、分配的内存,使整个系统保持一致性。在很多复杂情况下要保证这种正确性是有一定困难的。
一种简单的线程取消:取消线程调用一个取消线程的函数,被取消线程死亡。在这种情况下,被取消线程所持有的的资源得不到释放。取消线程负责保证被取消者处于可安全取消状态,在一个要求可靠性高的系统中,这种保证非常困难或者无法实现。这种取消称为不受限制的异步取消。
还存在另外一种更安全的线程取消机制。一个线程可以以可靠的受控制的方式向进程的其他线程发出取消请求,目标线程可以挂起这一请求使实际的取消动作在此后安全的时候进行,称为延迟取消。目标线程还可以定义其被取消后自动被系统调用的线程清除函数。
SylixOS兼容绝大多数POSIX接口, SylixOS中pthread_cancel函数执行线程取消功能。
pthread_cancel 函数和目标线程的取消动作是异步的。根据目标线程的取消属性不同,取消请求可能被忽略、立即执行或者延迟处理。为了清楚这些动作,下面知识点先简单介绍线程取消属性相关概念。
1.2 知识点
SylixOS中pthread_cancel函数由px_pthread.h头文件定义,其原型为:
int pthread_cancel (pthread_tthread);
- 此函数成功返回 0,失败返回错误号;
- 参数 thread 是取消的线程句柄。
SylixOS中用取消状态,取消类型和取消请求这3个元素共同表示一个线程的取消属性,其存在于线程控制块中。如表2-1所示。
表 2-1 取消说明
表示 |
说明 |
|
取消类型 |
允许取消 LW_THREAD_CANCEL_ENABLE |
线程取消状态决定了指定线程是否可以被取消,取消状态分为允许取消和禁止取消,一个线程设置为允许取消意味着此线程可以被取消,禁止取消意味着此线程只能从自己返回或者调用 pthread_exit函数来退出。 |
禁止取消 LW_THREAD_CANCEL_DISABLE |
||
取消类型 |
异步取消 LW_THREAD_CANCEL_ASYNCHRONOUS |
取消类型是线程取消的方式,分为异步取消和延迟取消,异步取消属性的线程会在收到取消信号时立即删除自身,也可说成立即取消,延迟取消属性的线程会在“安全”的时机调用线程删除函数删除自身。 |
延迟取消 LW_THREAD_CANCEL_DEFERRED |
||
取消请求 |
在延迟取消类型中,取消请求为1表示取消请求被挂起,直到运行到下一个取消点才被执行 |
该取消请求仅在线程允许取消时可用 |
线程初始化时会有默认的取消属性,即线程保留允许取消和延迟取消的属性,保证收到取消信号时,线程接受该取消信号,不会屏蔽掉,并且会在自身安全的时候,调用线程删除函数,即延迟取消。另外一个线程的取消状态和取消类型可分别调用相关函数设置,如表 2-2所示。函数均在px_pthread.h头文件中定义。
表 2-2 设置取消属性
函数 |
设置线程取消状态 int pthread_setcancelstate (int newstate, int *poldstate) |
设置线程取消类型 int pthread_setcanceltype (int newtype, int *poldtype) |
参数 |
参数 newstate 是新状态 输出参数 poldstate 返回之前的状态 |
参数 newtype 是新类型 输出参数 poldtype 返回之前的类型 |
返回值 |
此函数成功返回 0,失败返回错误号 |
此函数成功返回 0,失败返回非 0 值 |
前文提到,延迟请求会使线程的取消动作在安全的时候进行,那线程具体的取消时机是在什么时候呢?会涉及到“取消点”的概念,在后续章节中介绍。
2 技术实现
2.1 实现流程
SylixOS中pthread_cancel函数实现机制,如图3-1所示
图3-1 pthread_cancel函数实现流程
2.2 “取消点”概念
在使用延迟取消机制时,一个线程在可以被取消的地方定义取消点,当收到取消请求时,被取消的线程执行到取消点时退出,或者在一个取消点调用被阻塞时退出。
由于在延迟取消中必须在取消点才能被取消,这一限制可能使取消请求被挂起任意长时间。因此,如果某个调用可能使线程被阻塞或者进入某个较长时间的过程, POSIX 要求这些调用属于一个取消点,或者称这些调用为取消点调用,以防止取消请求陷入长时间等待,SylixOS中存在一些拥有取消点的函数,如open,read,pthread_join,printf等,他们都直接或间接的调用了pthread_testcancel函数pthread_testcancel函数内部实现流程如图 3-2所示。
图 3-2 pthread_testcancel函数实现流程
因此,被取消线程会在执行拥有取消点的函数时,进入到pthread_testcancel函数内部,进行如图 32所示的允许取消、延迟取消以及取消请求标志的判断流程,倘若条件满足,被取消线程会在这里调用线程删除函数删除自身。
表 3-1列出部分拥有取消点的函数以供参考。
表 3-1 拥有取消点的函数
函数名 |
函数名 |
函数名 |
sleep |
send |
open |
system |
sendmsg |
close |
wait |
aio_suspend |
read |
waitid |
mq_send |
pread |
waitpid |
mq_receive |
write |
wait3 |
pthread_barrier_wait |
pwrite |
wait4 |
sem_wait |
readv |
reclaimchild |
tcdrain |
writev |
accept4 |
fcntl |
fsync |
connect |
creat |
select |
recv |
sigtimedwait |
pause |
recvfrom |
pthread_join |
pthread_testcancel |
3 示例演示
以下示例验证均在SylixOS环境下进行。
3.1 示例1:立即取消
如图 4-1所示,子线程设置立即取消类型,那么主线程成功发送取消信号后,打印"pthread_cancel OK",子线程会在下次执行开始处删除自身,退出时子线程i的值可能为0~1000000任意值。
图4-1 立即取消示例
3.2 示例2:延迟取消
如图 4-2所示,子线程设置延迟取消类型,那么主线程执行取消线程函数后,打印“pthread_cancel OK”,子线程会在执行到取消点时删除自身,sleep为拥有取消点的函数,因此子线程退出时i 的值一定为1000000。(即pthread_cancel函数成功返回并不能代表目标线程已经退出)
图 4-2 延迟取消示例
倘若示例1和示例2都把子线程中入口函数中的sleep(1)这条语句去掉,那么示例1中的立即取消仍然有效;示例2中的延迟取消虽然主线程打印“pthread_cancel OK”,但是因为子线程的while(1)循环里没有“取消点”,子线程的取消请求一直都不能被处理,因此子线程并不会被成功取消,而是继续循环运行。如图 4-3所示。
图 4-3 没有“取消点”
不过我们可以手动加入pthread_testcancel函数进行取消请求处理,这样子线程也就有了一个“取消点”,在“取消点”取消请求被处理,线程即可退出。如图 4-4所示。
图 4-4 pthread_testcancel处理取消请求
4 总结
立即取消会通过信号方式修改目标线程任务上下文环境,即把旧目标线程任务上下文做
适当偏移,将信号处理句柄安装在上下文开始处,组成新的目标线程任务上下文,当任务调度轮到目标线程执行时,目标线程会优先执行信号处理程序,完成信号请求的动作。这里要注意时任务调度之后立即取消,因为任务调度切换任务上下文会花费一些时间,所以立即取消并不代表时间上的“立刻”。
延迟取消仅仅使目标线程的线程控制块相关标志修改,在目标线程执行到相关拥有取消点的函数时,进行取消请求检查,满足条件才会删除线程。即任务调度之后,目标线程还会继续执行任务上下文指定任务,直到碰到“取消点”,才可能取消线程。
pthread_cancel的成功返回仅仅表明调用该函数的线程完成了对目标线程线程控制块相关标志的修改,等到任务调度,目标线程才会检查这些标志,做出相应处理,即pthread_cancel的成功返回并不能代表目标线程的完全取消。
由此可见,线程取消并不是安全的,立即取消会让目标线程“立即退出”,延迟取消会给目标线程从开始到“取消点”争取一段执行时间,如果没有“取消点”,目标线程就不能完成预期的线程取消动作,这些都有可能造成很多不安全的因素,目标线程取消之前占有的资源如互斥锁,互斥锁没有释放线程就退出了,将导致别的线程无法获得改锁从而一直阻塞,更严重的还会造成死锁现象。
线程退出时可以调用线程清理处理程序,通过pthread_cleanup_pop和pthread_cleanup_push函数可以将有些“后事”在线程退出后交给线程清理处理程序帮忙处理,本文档此次主要浅析SylixOS的pthread_cancel流程,就不对线程清理处理程序如何使用做详细介绍了。
5 参考资料
SylixOS内核源码
《SylixOS应用程序开发手册》