嵌入式Linux-线程的回收/取消/分离

1. 线程的回收

1.1 回收线程的概念

春节七天连假已经过完啦,也该回收一下我们放假的线程了!
听过很多回收旧手机、旧冰箱和旧彩电…,那么回收线程又是什么呢?
在父、子进程当中,父进程可通过 wait()函数(或其变体 waitpid())阻塞等待子进程退出并获取其终止状态,回收子进程资源;而在线程当中,也需要如此,通过调用 pthread_join()函数来阻塞等待线程的终止,并获取线程的退出码,回收线程资源;pthread_join()函数原型如下所示:

#include 

int pthread_join(pthread_t thread, void **retval);

参数含义:

参数 含义
thread pthread_join()等待指定线程的终止,通过参数 thread(线程 ID)指定需要等待的线程
retval 如果参数 retval 不为 NULL,则 pthread_join()将目标线程的退出状态(即目标线程通过pthread_exit()退出时指定的返回值或者在线程 start 函数中执行 return 语句对应的返回值)复制到retval 所指向的内存区域;如果目标线程被 pthread_cancel()取消,则将 PTHREAD_CANCELED 放在retval 中。如果对目标线程的终止状态不感兴趣,则可将参数 retval 设置为 NULL

调用pthread_join()函数将会以阻塞的形式等待指定的线程终止,如果该线程已经终止,则pthread_join()立刻返回。如果多个线程同时尝试调用pthread_join()等待指定线程的终止,那么结果将是不确定的。

若线程并未分离则必须使用pthread_join()来等待线程终止,回收线程资源;如果线程终止后,其它线程没有调用pthread_join()函数来回收该线程,那么该线程将变成僵尸线程,与僵尸进程的概念相类似;同样,僵尸线程除了浪费系统资源外,若僵尸线程积累过多,那么会导致应用程序无法创建新的线程(这部分如果看不懂的地方可以看一下我博客有关僵尸进程的介绍)。

当然,如果进程中存在着僵尸线程并未得到回收,当进程终止之后,进程会被其父进程回收,所以僵尸线程同样也会被回收。

1.2 回收进程与回收线程的区别

我们来比对一下,有关进程回收和线程回收的区别吧,主要也是探讨关于pthread_join()执行的功能类似于针对进程的waitpid()调用:

  1. 线程之间关系是对等的。进程中的任意线程均可调用pthread_join()函数来等待另一个线程的终止。譬如,如果线程A创建了线程B,线程B再创建线程C,那么线程A可以调用pthread_join()等待线程C的终止,线程C也可以调用pthread_join()等待线程A的终止;这与进程间层次关系不同,父进程如果使用fork()创建了子进程,那么它也是唯一能够对子进程调用wait()的进程,线程之间不存在这样的关系。
  2. 不能以非阻塞的方式调用pthread_join()。对于进程,调用waitpid()既可以实现阻塞方式等待、也可以实现非阻塞方式等待。
#include 
#include 
#include 
#include 
#include 
#include 
#include 

static void *new_thread_start(void *arg)
{
	 printf("新线程 start\n");
	 sleep(2);
	 printf("新线程 end\n");
	 pthread_exit((void *)10);
}

int main(void)
{
	 pthread_t tid;
	 void *tret;
	 int ret;
	 ret = pthread_create(&tid, NULL, new_thread_start, NULL);
	 if (ret) 
	 {
		 fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
		 exit(-1);
	 }
	 ret = pthread_join(tid, &tret);
	 if (ret) 
	 {
		 fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
		 exit(-1);
	 }
	 printf("新线程终止, code=%ld\n", (long)tret);
	 exit(0);
}

主线程调用 pthread_create()创建新线程之后,新线程执行 new_thread_start()函数,而在主线程中调用pthread_join()阻塞等待新线程终止,新线程终止后,pthread_join()返回,将目标线程的退出码保存在*tret 所指向的内存中。测试结果如下:
在这里插入图片描述

2. 取消线程

2.1 取消线程的概念

在通常情况下,进程中的多个线程会并发执行,每个线程各司其职,直到线程的任务完成之后,该线程中会调用pthread_exit()退出,或在线程start函数执行return语句退出。
有时候,在程序设计需求当中,需要向一个线程发送一个请求,要求它立刻退出,我们把这种操作称为取消线程,也就是向指定的线程发送一个请求,要求其立刻终止、退出。譬如,一组线程正在执行一个运算,一旦某个线程检测到错误发生,需要其它线程退出,取消线程这项功能就派上用场了。

2.2 取消线程函数

通过调用pthread_cancel()库函数向一个指定的线程发送取消请求,其函数原型如下所示:

#include 

int pthread_cancel(pthread_t thread);

发出取消请求之后,函数 pthread_cancel()立即返回,不会等待目标线程的退出。默认情况下,目标线程也会立刻退出,其行为表现为如同调用了参数为 PTHREAD_CANCELED(其实就是(void *)-1)的pthread_exit()函数,但是,线程可以设置自己不被取消或者控制,所以pthread_cancel()并不会等待线程终止,仅仅只是提出请求。

#include 
#include 
#include 
#include 
#include 
#include 
#include 
static void *new_thread_start(void *arg)
{
 	printf("新线程--running\n");
 	for ( ; ; )
	sleep(1);
	return (void *)0;
}

int main(void)
{
	 pthread_t tid;
	 void *tret;
	 int ret;
	 /* 创建新线程 */
	 ret = pthread_create(&tid, NULL, new_thread_start, NULL);
	 if (ret) 
	 {
		 fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
		 exit(-1);
	 }
	 sleep(1);
	 /* 向新线程发送取消请求 */
	 ret = pthread_cancel(tid);
	 if (ret) 
	 {
		 fprintf(stderr, "pthread_cancel error: %s\n", strerror(ret));
		 exit(-1);
	 }
	 /* 等待新线程终止 */
	 ret = pthread_join(tid, &tret);
	 if (ret) 
	 {
		 fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
		 exit(-1);
	 }
	 printf("新线程终止, code=%ld\n", (long)tret);
	 exit(0);
}

解读程序: 主线程创建新线程,新线程 new_thread_start()函数直接运行 for 死循环;主线程休眠一段时间后,调用pthread_cancel()向新线程发送取消请求,接着再调用 pthread_join()等待新线程终止、获取其终止状态,将线程退出码打印出来。测试结果如下:
在这里插入图片描述
由打印结果可知,当主线程发送取消请求之后,新线程便退出了,而且退出码为-1,也就是PTHREAD_CANCELED.

2.3 取消状态以及类型

默认情况下,线程是响应其它线程发送过来的取消请求的,响应请求然后退出线程。当然,线程可以选择不被取消或者控制如何被取消,通过**pthread_setcancelstate()pthread_setcanceltype()**来设置线程的取消性状态和类型。

#include 

int pthread_setcancelstate(int state, int *oldstate);
int pthread_setcanceltype(int type, int *oldtype);

使用这些函数需要包含头文件pthread_setcancelstate()函数会将调用线程的取消性状态设置为参数state中给定的值,并将线程之前的取消性状态保存在参数oldstate指向的缓冲区中,如果对之前的状态不感兴趣,Linux允许将参数oldstate设置为NULL;pthread_setcancelstate()调用成功将返回0,失败返回非0值的错误码。

pthread_setcancelstate()函数执行的设置取消性状态和获取旧状态操作,这两步是一个原子操作。参数state必须是以下值之一:

  1. PTHREAD_CANCEL_ENABLE:线程可以取消,这是新创建的线程取消性状态的默认值,所以新建线程以及主线程默认都是可以取消的。
  2. PTHREAD_CANCEL_DISABLE:线程不可被取消,如果此类线程接收到取消请求,则会将请求挂起,直至线程的取消性状态变为PTHREAD_CANCEL_ENABLE。

在新线程的new_thread_start()函数中调用pthread_setcancelstate()函数将线程的取消性状态设置为PTHREAD_CANCEL_DISABLE,我们来试试,此时主线程还能不能取消新线程,示例代码如下所示:

#include 
#include 
#include 
#include 
#include 
#include 
#include 
static void *new_thread_start(void *arg)
{
	 /* 设置为不可被取消 */
	 pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);
	 for ( ; ; ) 
	 {
		 printf("新线程--running\n");
		 sleep(2);
	 }
	 return (void *)0;
}
int main(void)
{
	 pthread_t tid;
	 void *tret;
	 int ret;
	 /* 创建新线程 */
	 ret = pthread_create(&tid, NULL, new_thread_start, NULL);
	 if (ret) 
	 {
		 fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
		 exit(-1);
	 }
	 sleep(1);
	 /* 向新线程发送取消请求 */
	 ret = pthread_cancel(tid);
	 if (ret) 
	 {
		 fprintf(stderr, "pthread_cancel error: %s\n", strerror(ret));
		 exit(-1);
	 }
	 /* 等待新线程终止 */
	 ret = pthread_join(tid, &tret);
	 if (ret) 
	 {
		 fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
		 exit(-1);
	 }
	 printf("新线程终止, code=%ld\n", (long)tret);
	 exit(0);
}

嵌入式Linux-线程的回收/取消/分离_第1张图片
测试结果确实如此,将一直重复打印"新线程–running",因为新线程是一个死循环(测试完成按 Ctrl+C退出)。

在介绍一下pthread_setcanceltype()函数
如果线程的取消性状态为PTHREAD_CANCEL_ENABLE,那么对取消请求的处理则取决于线程的取消性类型,该类型可以通过调用pthread_setcanceltype()函数来设置,它的参数type指定了需要设置的类型,而线程之前的取消性类型则会保存在参数oldtype所指向的缓冲区中,如果对之前的类型不敢兴趣,Linux 下允许将参数oldtype设置为NULL。同样pthread_setcanceltype()函数调用成功将返回0,失败返回非0值的错误码。

pthread_setcanceltype()函数执行的设置取消性类型和获取旧类型操作,这两步是一个原子操作。参数type必须是以下值之一:

  1. PTHREAD_CANCEL_DEFERRED:取消请求到来时,线程还是继续运行,取消请求被挂起,直到线程到达某个取消点(cancellation point)为止,这是所有新建线程包括主线程默认的取消性类型。
  2. PTHREAD_CANCEL_ASYNCHRONOUS:可能会在任何时间点(也许是立即取消,但不一定)取消线程,这种取消性类型应用场景很少,不再介绍!

当某个线程调用fork()创建子进程时,子进程会继承调用线程的取消性状态和取消性类型,而当某线程调用exec函数时,会将新程序主线程的取消性状态和类型重置为默认值,也就是PTHREAD_CANCEL_ENABLE和PTHREAD_CANCEL_DEFERRED。

2.4 取消点

若将线程的取消性类型设置为PTHREAD_CANCEL_DEFERRED时(线程可以取消状态下),收到其它线程发送过来的取消请求时,仅当线程抵达某个取消点时,取消请求才会起作用。

那什么是取消点呢?所谓取消点其实就是一系列函数,当执行到这些函数的时候,才会真正响应取消请求,这些函数就是取消点;在没有出现取消点时,取消请求是无法得到处理的,究其原因在于系统认为,但没有到达取消点时,线程此时正在执行的工作是不能被停止的,正在执行关键代码,此时终止线程将可能会导致出现意想不到的异常发生。

取消点函数包括哪些呢?下表给大家简单地列出了一些:
嵌入式Linux-线程的回收/取消/分离_第2张图片

除了表 中所列函数之外,还有大量的函数,系统实现可以将其作为取消点,这里便不再一一列举出来了,大家也可以通过 man 手册进行查询,命令为"man 7 pthreads":

man 7 pthreads

嵌入式Linux-线程的回收/取消/分离_第3张图片
线程在调用这些函数时,如果收到了取消请求,那么线程便会遭到取消;除了这些作为取消点的函数之外,不得将任何其它函数视为取消点(亦即,调用这些函数不会招致取消)。

2.5 线程可取消性的检测

假设线程执行的是一个不含取消点的循环(譬如 for 循环、while 循环),那么这时线程永远也不会响应取消请求,也就意味着除了线程自己主动退出,其它线程将无法通过向它发送取消请求而终止它.

在实际应用程序当中,确实会遇到这种情况,线程最终运行在一个循环当中,该循环体内执行的函数不存在任何一个取消点,但实际项目需求是:该线程必须可以被其它线程通过发送取消请求的方式终止,那这个时候怎么办?此时可以使用 pthread_testcancel()函数,该函数目的很简单,就是产生一个取消点,线程如果已有处于挂起状态的取消请求,那么只要调用该函数,线程就会随之终止。其函数原型如下所示:

#include 

void pthread_testcancel(void);

下面我们通过一个小demo看看如何吧:

  1. 主线程创建一个新的进程,新进程的取消性状态和类型置为默认,新进程最终执行的是一个不含取消点的循环;主线程向新线程发送取消请求,示例代码如下所示:
#include 
#include 
#include 
#include 
#include 
#include 
#include 
static void *new_thread_start(void *arg)
{
	 printf("新线程--start run\n");
	 for ( ; ; ) {
	 }
	 return (void *)0;
}

int main(void)
{
	 pthread_t tid;
	 void *tret;
	 int ret;
	 /* 创建新线程 */
	 ret = pthread_create(&tid, NULL, new_thread_start, NULL);
	 if (ret) 
	 {
		 fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
		 exit(-1);	
	 }
	 sleep(1);
	 /* 向新线程发送取消请求 */
	 ret = pthread_cancel(tid);
	 if (ret) 
	 {
		 fprintf(stderr, "pthread_cancel error: %s\n", strerror(ret));
		 exit(-1);
	 }
	 /* 等待新线程终止 */
	 ret = pthread_join(tid, &tret);
	 if (ret) 
	 {
		 fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
		 exit(-1);
	 }
	 printf("新线程终止, code=%ld\n", (long)tret);
	 exit(0);
}

解读代码:
新线程的 new_thread_start()函数中是一个 for 死循环,没有执行任何函数,所以是一个没有取消点的循环体,主线程调用 pthread_cancel()是无法将其终止的;
在这里插入图片描述

  1. 在new_thread_start 函数的 for 循环体中执行 pthread_testcancel()函数,接下来我们再试试:
#include 
#include 
#include 
#include 
#include 
#include 
#include 
static void *new_thread_start(void *arg)
{
	 printf("新线程--start run\n");
	 for ( ; ; ) {
	 pthread_testcancel();
	 }
 	 return (void *)0;
}
int main(void)
{
	 pthread_t tid;
	 void *tret;
	 int ret;
	 /* 创建新线程 */
	 ret = pthread_create(&tid, NULL, new_thread_start, NULL);
	 if (ret) 
	 {
		 fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
		 exit(-1);
	 }
	 sleep(1);
	 /* 向新线程发送取消请求 */
	 ret = pthread_cancel(tid);
	 if (ret) 
	 {
		 fprintf(stderr, "pthread_cancel error: %s\n", strerror(ret));
		 exit(-1);
	 }
	 /* 等待新线程终止 */
	 ret = pthread_join(tid, &tret);
	 if (ret) 
	 {
		 fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
		 exit(-1);
	 }
	 printf("新线程终止, code=%ld\n", (long)tret);
	  exit(0);
}

如果 pthreadtestcancel()可以产生取消点,那么主线程便可以终止新线程,测试结果如下:
在这里插入图片描述
从这里我们能清晰的反应得到,新线程是有取消点可以正常退出的!!!

3. 分离线程

3.1 如何分离线程

默认情况下,当线程终止时,其它线程可以通过调用pthread_join()获取其返回状态、回收线程资源,有时,程序员并不关心♥线程的返回状态,只是希望系统在线程终止时能够自动回收线程资源并将其移除。在这种情况下,可以调用pthread_detach()将指定线程进行分离,也就是分离线程,pthread_detach()函数原型如下所示:

#include 

int pthread_detach(pthread_t thread);

使用该函数需要包含头文件,参数 thread 指定需要分离的线程,函数 pthread_detach()调用成功将返回 0;失败将返回一个错误码。

一个线程既可以将另一个线程分离,同时也可以将自己分离,譬如:

pthread_detach(pthread_self());

tips: 一旦线程处于分离状态,就不能再使用 pthread_join()来获取其终止状态,此过程是不可逆的,一旦处于分离状态之后便不能再恢复到之前的状态。处于分离状态的线程,当其终止后,能够自动回收线程资源。

#include 
#include 
#include 
#include 
#include 
#include 
#include 
static void *new_thread_start(void *arg)
{
	 int ret;
	 /* 自行分离 */
	 ret = pthread_detach(pthread_self());
	 if (ret) 
	 {
		 fprintf(stderr, "pthread_detach error: %s\n", strerror(ret));
		 return NULL;
 	 }
	 printf("新线程 start\n");
	 sleep(2); //休眠 2 秒钟
	 printf("新线程 end\n");
	 pthread_exit(NULL);
}
int main(void)
{
	 pthread_t tid;
	 int ret;
	 /* 创建新线程 */
	 ret = pthread_create(&tid, NULL, new_thread_start, NULL);
	 if (ret) 
	 {
		 fprintf(stderr, "pthread_create error: %s\n", strerror(ret));
		 exit(-1);
	 }
	 sleep(1); //休眠 1 秒钟
	 /* 等待新线程终止 */
	 ret = pthread_join(tid, NULL);
	 if (ret)
	 fprintf(stderr, "pthread_join error: %s\n", strerror(ret));
	 pthread_exit(NULL);
}

代码解析:主线程创建新的线程之后,休眠1秒钟,调用pthread_join()等待新线程终止;新线程调用pthread_detach(pthread_self())将自己分离,休眠2秒钟之后pthread_exit()退出线程;主线程休眠1秒钟是能够确保调用pthread_join()函数时新线程已经将自己分离了,所以按照上面的介绍可知,此时主线程调用pthread_join()必然会失败,测试结果如下:

在这里插入图片描述
打印结果正如我们所料,主线程调用pthread_join()确实会出错,错误提示为“Invalid argument”。

本文参考正点原子的嵌入式LinuxC应用编程。

你可能感兴趣的:(嵌入式Linux学习,linux)