Linux -- 多线程编程之 - 基础实现二

  书接上文,我们继续说明关于线程退出的问题。

三、线程的退出

3.1、概述

  新创建的线程可以以以下的任意一种方式终止,他们分别是:

  • 1、线程自身调用线程退出函数 pthread_exit() 函数,并且可以用于指定一个退出状态值,该值可用于同一进程中调用 pthread_join() 函数的另一个线程。需要说明的是,这种退出方式是线程的主动行为。点击可直接跳转并查看详情

  • 2、线程自身从 start_routine 指向的函数返回,其实就是线程执行完流程那之后自己返回的操作,相当于使用 return 语句中提供的值调用 pthread_exit() 函数。点击可直接跳转并查看详情

  • 3、线程自身被取消,也就是其他线程调用函数 pthread_cancel() 并且指定要取消的线程的线程 ID这对于被取消的线程来说是一种被动行为。点击可直接跳转并查看详情

  • 4、进程中的任何线程调用 exit()/_Exit() 函数退出,或者主线程执行 main() 函数返回。需要注意的是,这种操作将会导致进程结束,并且此进程中所有线程的终止。点击可直接跳转并查看详情

3.2、调用pthread_exit()函数

3.2.1、pthread_exit()函数原型

  pthread_exit() 函数原型如下所示。

#include <pthread.h>
void pthread_exit(void *retval);

Compile and link with -pthread.

功能:
  结束调用者所在的线程
参数:
  retval  :返回线程退出状态值,在其他线程中如果有线程 pthread_join() 函数则可以访问到这个指针
返回:
  函数无返回


唯一的可移植方法是调用 pthread_exit()

  提到 pthread_exit() 函数,那么同时也需要介绍另外一个搭档函数,pthread_join() 函数原型如下所示。

3.2.2、pthread_join()函数原型

#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);

Compile and link with -pthread.

功能:
  等待一个指定的线程退出,如果该线程已经终止,那么函数将立即返回。
参数:
  thread  :等待的线程 ID
  retval  :承接 pthread_exit() 的退出状态值
返回:
  成功,返回 0
  失败,返回错误编号

说明:

  • 1、函数指定的线程必须是可接合的。
  • 2、如果 retval 不为 NULL ,则 pthread_join()
    将目标线程的退出状态(即目标线程给 pthread_exit() 的参数值)复制到 retval 所指向的位置。如果目标线程被取消,那么 PTHREAD_CANCELED 将被放置在 retval 指向的位置。
  • 3、如果多个线程同时尝试与同一线程联接,则结果未定义。如果调用 pthread_join() 的线程被取消,那么目标线程将保持可连接状态(即,它不会被分离)。
  • 4、调用线程将一直阻塞,直到指定的线程调用 pthread_exit()、从启动例程中返回或者被取消。如果线程简单地从它的启动例程返回,那么 retval 就包含返回码。如果线程被取消,由 retval 指定的内存单元就设置为 PTHREAD_CANCELED
  • 5、如果线程已经处于分离状态, pthread_join() 调用就会失败,返回 EINVAL,如果对线程的返回值并不关心,那么可以把 retval 设置为 NULL。在这种情况下,调用pthread_join() 函数可以等待指定的线程终止,但并不获取线程的终止状态。

3.2.3、示例代码

  编写一个简单的程序,程序主要的是实现的功能描述如下。
  创建两个线程并且设置两个不同的线程执行函数,其中第一个线程执行函数调用 pthread_exit()函数并设置线程返回值,第二个线程设置线程返回值为NULL,然后在主线程中调用 pthread_join() 函数等待两个线程的退出并且输出线程的返回值。

  实现的代码如下所示。

#include <pthread.h> /* for pthread_create */
#include <stdio.h>	 /* for printf */
#include <stdlib.h>	 /* for exit */
#include <unistd.h>	 /* for usleep */

/* 线程的执行体函数 -- 模拟有返回值 */
void *callback_func_ret(void *arg)
{
	/* 临时变量,用于循环计数 */
	int idx = 0;
	/* 将传入的参数进行转换 */
	int cnt = *(int *)arg;
	/* 定义线程的返回值,用于测试,下面详细说明 */
	char *retval = "callback_func Exited!";
	for (idx = 0; idx < cnt; idx++)
	{
		/* 延时500ms */
		usleep(500000);
		/* 输出调试信息,并线程ID */
		printf("I'm sub thread, my ID = %lX\n", pthread_self());
	}
	/* 线程退出并返回值 */
	pthread_exit((void *)retval);
}

/* 线程的执行体函数 -- 模拟无返回值 */
void *callback_func_null(void *arg)
{
	/* 延时500ms,确定第一个线程先开始执行 */
	usleep(50000);

	/* 临时变量,用于循环计数 */
	int idx = 0;
	/* 将传入的参数进行转换 */
	int cnt = *(int *)arg;
	for (idx = 0; idx < cnt; idx++)
	{
		/* 延时500ms */
		usleep(500000);
		/* 输出调试信息,并线程ID */
		printf("I'm sub thread, my ID = %lX\n", pthread_self());
	}
	/* 线程退出并返回值 */
	pthread_exit(NULL);
}

int main(int argc, const char *argv[])
{
	/* 定义变量,用于存储线程ID */
	pthread_t pthid1 = 0, pthid2 = 0;
	/* 定义一个变量,用于接收返回值 */
	char *retval1 = NULL, *retval2 = NULL;
	/* 定义线程函数的参数 */
	int counter = 1;
	/* 输出调试语句 */
	printf("pthread exit(pthread_exit) test!\n");

	/* 创建第一个测试线程 */
	if (pthread_create(&pthid1, NULL, callback_func_ret, &counter) != 0)
	{
		perror("pthread_create error"); /* 打印错误信息 */
		exit(EXIT_FAILURE);				/* 结束整个进程 */
	}
	/* 创建第二个测试线程 */
	if (pthread_create(&pthid2, NULL, callback_func_null, &counter) != 0)
	{
		perror("pthread_create error"); /* 打印错误信息 */
		exit(EXIT_FAILURE);				/* 结束整个进程 */
	}

	/* 等待指定的线程退出并记录其返回值 */
	pthread_join(pthid1, (void **)&retval1);
	pthread_join(pthid2, (void **)&retval2);

	printf("pthid1 = %lX, retval = %s\n", pthid1, retval1);
	printf("pthid2 = %lX, retval = %s\n", pthid2, retval2);

	return 0;
}

  编译上述程序并运行,程序执行的效果如下图3.1所示。

图3.1 程序执行效果图

  由上图可以看到,当一个线程通过调用 pthread_exit() 退出时,进程中的其他线程可以通过调用 pthread_join() 函数获得该线程的退出状态。所以至此功德算是已经圆满。

说明

  pthread_create()pthread_exit() 函数的无类型指针(void* )参数可以传递的值不止一个,这个指针可以传递包含复杂信息的结构的地址,但是注意,这个结构所使用的内存在调用者完成调用以后必须仍然是有效的
  例如,调用线程的栈上分配了该结构,那么其他的线程在使用这个结构时内存内容可能已经改变了。

  又如,在上面的示例代码中,线程一在自己的栈上分配了一个结构,然后把指向这个结构的指针传给 和 pthread_exit() ,那么调用 和 pthread_join() 的线程试图使用该结构时,这个栈有可能已经被撤销,这块内存也已另作他用。

3.3、调用pthread_cancel()函数

3.3.1、pthread_cancel()函数原型

  pthread_cancel()的函数原型如下所示。

#include <pthread.h>
int pthread_cancel(pthread_t thread);

Compile and link with -pthread.

功能:
  向线程发送一个取消请求,但是目标线程是否以及何时响应取消请求取决于该线程控制的两个属性:其可取消性 statetype
参数:
  thread  :目标线程的线程 ID
返回:
  成功,返回 0
  失败,返回一个非零的错误

3.3.2、pthread_setcancelstate()函数原型

#include <pthread.h>
int pthread_setcancelstate(int state, int *oldstate);

Compile and link with -pthread.

功能:
  将调用线程的可取消性状态设置为 state 中给定的值
参数:
  state         :要设置的线程的可取消状态值
  oldstate:返回上一个可取消状态值
返回:
  成功 :返回 0
  失败 :返回一个非零值

  如果函数的参数 state 值无效,那么 pthread_setcancelstate() 函数可能会执行失败,并且返回 EINVAL

  所以,参数 state 的值必须为以下值之一:

  • PTHREAD_CANCEL_ENABLE:该线程是可取消的。这是所有新线程(包括初始线程)中的默认可取消状态。线程的可取消性类型确定可取消线程何时响应取消请求。
  • PTHREAD_CANCEL_DISABLE:该线程不可取消。如果收到取消请求,将阻止该请求,直到启用取消功能。

3.3.3、pthread_setcanceltype()函数原型

#include <pthread.h>
int pthread_setcanceltype(int type, int *oldtype);

Compile and link with -pthread.

功能:
  将调用线程的可取消性类型设置为 type 中给定的值
参数:
  state         :要设置的线程的可取消类型
  oldstate:返回上一个可取消类型
返回:
  成功 :返回 0
  失败 :返回一个非零值

  如果函数的参数 type 值无效,那么 pthread_setcanceltype() 函数可能会执行失败,并且返回 EINVAL

  所以,参数 type 的值必须为以下值之一:

  • PTHREAD_CANCEL_DEFERRED:取消请求被延迟,直到线程下一次调用作为取消点的函数为止。这是所有新线程(包括初始线程)中的默认可取消性类型。即使使用延迟取消,异步信号处理程序中的取消点仍可能被操作,其效果就像异步取消一样。
  • PTHREAD_CANCEL_ASYNCHRONOUS:线程可以随时取消(通常在收到取消请求后会立即取消,但系统不保证这一点)

      对请求的取消执行操作时,线程将执行以下步骤(按此顺序):

  • 1、将 poped 取消清理处理程序(与 pushed 的顺序相反)。相关函数的原型如下所示。
#include <pthread.h>
void pthread_cleanup_push(void (*routine)(void *), void *arg);
void pthread_cleanup_pop(int execute);

Compile and link with -pthread.

详情请自行查阅相关功能与用法,本文不再进行展开。

  • 2、以未指定的顺序调用特定于线程的数据析构函数。

  • 3、线程终止。

  上述步骤与 pthread_cancel() 调用异步发生;pthread_cancel() 的返回状态仅通知调用方取消请求是否已成功排队。

  被取消的线程终止后,使用 pthread_join() 与该线程的连接将获得 PTHREAD_CANCELED 作为线程的退出状态。

  我们可以在 pthread.h 文件中查到 PTHREAD_CANCEL_ENABLEPTHREAD_CANCEL_DISABLEPTHREAD_CANCELED 的定义:

enum
{
	PTHREAD_CANCEL_ENABLE,
#define PTHREAD_CANCEL_ENABLE PTHREAD_CANCEL_ENABLE
	PTHREAD_CANCEL_DISABLE
#define PTHREAD_CANCEL_DISABLE PTHREAD_CANCEL_DISABLE
};
enum
{
	PTHREAD_CANCEL_DEFERRED,
#define PTHREAD_CANCEL_DEFERRED PTHREAD_CANCEL_DEFERRED
	PTHREAD_CANCEL_ASYNCHRONOUS
#define PTHREAD_CANCEL_ASYNCHRONOUS PTHREAD_CANCEL_ASYNCHRONOUS
};

#define PTHREAD_CANCELED ((void *) -1)

  对于接收 Cancel 信号后结束执行的目标线程,等同于该线程自己执行 pthread_exit(PTHREAD_CANCELED); 语句:

3.3.4、代码示例

#include <pthread.h> /* for pthread_create */
#include <stdio.h>	 /* for printf */
#include <stdlib.h>	 /* for exit */
#include <unistd.h>	 /* for usleep */

/* 线程的执行体函数 -- 函数执行完成后自动退出 */
void *callback_func_exit(void *arg)
{
	/* 输出调试信息,并线程ID */
	printf("I'm callback_func_exit, my ID = %lX\n", pthread_self());

	/* 线程自动退出并设置返回值为 PTHREAD_CANCELED */
	pthread_exit(PTHREAD_CANCELED);
}

/* 线程的执行体函数 -- 延时等被取消 */
void *callback_func_cancel(void *arg)
{
	/* 输出调试信息,并线程ID */
	printf("I'm callback_func_cancel, my ID = %lX\n", pthread_self());
	/**************************************************************
	 *  有兴趣的可以测试一下用 while(1) 的程序测试 pthread_cancel 功能 
	 *  程序在执行 while(1) 的过程中是不可以用 pthread_cancel 函数取消的
	 *  其实中之中涉及到一个  取消点  的概念,需要在实际编程中注意
	**************************************************************/
	/* 让线程永久执行,但是每次循环延时1s,测试取消的效果 */
	while (1)
	{
		sleep(1);
	}
	/* 如果用下面的测试程序使用while(1),那么请先注释掉上面的 sleep() 函数*/
	// while (1);
}

/* 线程的执行体函数 -- 设置线程为不可取消状态 */
void *callback_func_discanceled(void *arg)
{
	/* 定义用于记录函数执行的返回值 */
	int ret = 0;
	/* 定义用于记录线程的上一次的可取消状态 */
	int oldstate = 0;

	/* 输出调试信息,并线程ID */
	printf("I'm callback_func_discanceled, my ID = %lX\n", pthread_self());

	/* 设置线程的可取消状态为 PTHREAD_CANCEL_DISABLE */
	ret = pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldstate);
	if (ret != 0)
	{
		perror("pthread_join pthid1 return failed!\n");
		exit(EXIT_FAILURE); /* 结束整个进程 */
	}
	/* 调试输出 */
	printf("pthread_setcancelstate return succeed, oldstate = %d\n", oldstate);

	/* 延时50s,测试时间尽可能的长一点测试取消的效果 */
	sleep(20);
}

int main(int argc, const char *argv[])
{
	/* 定义变量,用于存储线程ID */
	pthread_t pthid1 = 0;
	pthread_t pthid2 = 0;
	pthread_t pthid3 = 0;
	/* 定义变量,用于接收返回值 */
	void *retval1 = NULL;
	void *retval2 = NULL;
	void *retval3 = NULL;
	/* 定义用于记录函数执行的返回值 */
	int ret = 0;

	/* 输出调试语句 */
	printf("pthread exit(pthread_cancel) test!\n");

	/* 创建第一个测试线程 */
	if (pthread_create(&pthid1, NULL, callback_func_exit, NULL) != 0)
	{
		perror("pthread_create error"); /* 打印错误信息 */
		exit(EXIT_FAILURE);				/* 结束整个进程 */
	}
	/* 延时500ms,确定第一个线程先开始执行 */
	usleep(50000);
	/* 创建第二个测试线程 */
	if (pthread_create(&pthid2, NULL, callback_func_cancel, NULL) != 0)
	{
		perror("pthread_create error"); /* 打印错误信息 */
		exit(EXIT_FAILURE);				/* 结束整个进程 */
	}
	/* 延时500ms,确定前面线程先开始执行 */
	usleep(50000);
	/* 创建第三个测试线程 */
	if (pthread_create(&pthid3, NULL, callback_func_discanceled, NULL) != 0)
	{
		perror("pthread_create error"); /* 打印错误信息 */
		exit(EXIT_FAILURE);				/* 结束整个进程 */
	}

	/* 主线程延时片刻,等待子线程相关运行完成 */
	sleep(2);

	/************************* 测试第一个线程 *******************************************************/
	/* 等待指定的线程退出并记录其返回值 */
	ret = pthread_join(pthid1, (void **)&retval1);
	if (ret != 0)
	{
		perror("pthread_join pthid1 return failed!\n");
		exit(EXIT_FAILURE); /* 结束整个进程 */
	}
	/* 调试信息,并输出线程ID以及返回值,为了输出好看,分开输出 */
	printf("\ncallback_func_exit joined succeed!\n");
	printf("\tpthid = %lX, retval = %ld\n", pthid1, (long)retval1);

	/************************* 测试第二个线程 *******************************************************/
	/* 向线程 pthid2 发送信号 */
	ret = pthread_cancel(pthid2);
	if (ret != 0)
	{
		perror("pthread_cancel return failed!\n");
		exit(EXIT_FAILURE); /* 结束整个进程 */
	}
	/* 等待指定的线程退出并记录其返回值 */
	ret = pthread_join(pthid2, (void **)&retval2);
	if (ret != 0)
	{
		perror("pthread_join pthid2 return failed!\n");
		exit(EXIT_FAILURE); /* 结束整个进程 */
	}

	/* 调试信息,并输出线程ID以及返回值,为了输出好看,分开输出 */
	printf("\ncallback_func_cancel joined succeed!\n");
	printf("\tpthid = %lX, retval = %ld\n", pthid1, (long)retval1);

	/************************* 测试第三个线程 *******************************************************/
	/* 获取当前的系统时间,用于记录线程的退出的时间 */
	time_t tt;
	time(&tt);
	struct tm *t = localtime(&tt);

	/* 向线程 pthid3 发送信号 */
	ret = pthread_cancel(pthid3);
	if (ret != 0)
	{
		perror("pthread_cancel return failed!\n");
		exit(EXIT_FAILURE); /* 结束整个进程 */
	}

	/* 调试信息,并输出线程ID以及返回值,为了输出好看,分开输出 */
	printf("\nJoined callback_func_discanceled... Current time is : %02d:%02d:%02d\n",
		   t->tm_hour, t->tm_min, t->tm_sec);

	/* 等待指定的线程退出并记录其返回值 */
	ret = pthread_join(pthid3, (void **)&retval3);
	if (ret != 0)
	{
		perror("pthread_join pthid2 return failed!\n");
		exit(EXIT_FAILURE); /* 结束整个进程 */
	}
	/* 获取当前的系统时间,用于记录线程的退出的时间 */
	time(&tt);
	t = localtime(&tt);
	/* 调试信息,并输出线程ID以及返回值,为了输出好看,分开输出 */
	printf("callback_func_discanceled joined succeed!   Time is : %02d:%02d:%02d\n",
		   t->tm_hour, t->tm_min, t->tm_sec);
	printf("\tpthid = %lX, retval = %ld\n", pthid3, (long)retval3);

	return 0;
}

  编译上述程序并运行,程序执行的效果如下图3.2所示。

图3.2 程序执行效果图

  在默认情况下,线程的终止状态会保存直到对该线程调用 pthread_join() 函数。如果线程已经被分离,线程的底层存储资源可以在线程终止时立即被收回。在线程被分离后,我们不能用 pthread_join() 函数等待它的终止状态,因为对分离状态的线程调用 pthread_join() 会产生未定义行为。可以调用 pthread_detach() 分离线程,函数原型如下所示。

#include <pthread.h>
int pthread_detach(pthread_t thread);

Compile and link with -pthread.

功能:
  将线程标识的线程标记为已分离
参数:
  thread :要标记的线程的线程ID
返回:
  成功,返回0
  失败:返回错误码

说明:

  • 当分离的线程终止时,其资源将自动释放回系统,而不需要另一个线程与终止的线程连接。
  • 尝试分离已分离的线程会导致未指定的行为。
  • 函数返回错误码如下定义:
      EINVAL: thread参数指向的不是可接合的线程
      ESRCH   : 找不到线程IDthread的线程

3.4、exit/_Exit/_exit

关于函数,详细可以参考博文: Linux – exit()函数、_exit()函数、return的说明与使用

  当程序执行到 exit() 函数或者 _exit() 函数时候,进程无条件停止剩下的所有操作,清除各种数据结构,并终止本进程的运行

  所以需要的注意的是,在使用线程函数时候,不能直接使exit()函数退出,因为exit()函数的作用是推出当前的进程,通常一个进程包含多个线程,如果调用了exit()函数,该进程中所有的线程都会结束,因此,在线程中使用 pthread_exit() 函数替代了exit() 函数结束当前的线程。

  当时如果当前线程在执行相关任务错误或者出现异常的情况下需要整个进程退出,并且主线程不关心具体的异常的时候,可以直接使用exit()函数族退出当前进程。

  函数族功能比较简单直接,所以不再进行相关的代码进行演示。

3.5、线程返回

  线程最简单的一种退出方式,就是从启动例程中返回,其实就是在回调函数中直接返回即表明此线程执行完成,返回值则为线程的退出码。

  用下面的简单的一个程序实现线程的自主返回,并且设置返回值为特定的字符串。程序实现的代码如下所示。

#include <pthread.h> /* for pthread_create */
#include <stdio.h>	 /* for printf */
#include <stdlib.h>	 /* for exit */
#include <unistd.h>	 /* for usleep */

/* 设置线程的返回的值 */
char *thread_return = "thread return succeed!";

/* 线程的执行体函数 -- 函数执行完成后自动退出 */
void *callback_func_return(void *arg)
{
	/* 输出调试信息,并线程ID */
	printf("I'm callback_func_exit, my ID = %lX\n", pthread_self());

	/* 线程返回并设置返回值 */
	return thread_return;
}

int main(int argc, const char *argv[])
{
	/* 定义变量,用于存储线程ID */
	pthread_t pthid = 0;
	/* 定义变量,用于接收返回值 */
	void *retval = NULL;
	/* 定义用于记录函数执行的返回值 */
	int ret = 0;

	/* 输出调试语句 */
	printf("pthread exit(pthread return) test!\n");

	/* 创建第一个测试线程 */
	if (pthread_create(&pthid, NULL, callback_func_return, NULL) != 0)
	{
		perror("pthread_create error"); /* 打印错误信息 */
		exit(EXIT_FAILURE);				/* 结束整个进程 */
	}

	/* 主线程延时片刻,等待子线程相关运行完成 */
	sleep(1);

	/************************* 测试第一个线程 *******************************************************/
	/* 等待指定的线程退出并记录其返回值 */
	ret = pthread_join(pthid, (void **)&retval);
	if (ret != 0)
	{
		perror("pthread_join pthid1 return failed!\n");
		exit(EXIT_FAILURE); /* 结束整个进程 */
	}

	/* 调试信息,并输出线程ID以及返回值,为了输出好看,分开输出 */
	printf("\ncallback_func_return joined succeed!\n");
	printf("\tpthid = %lX, retval = %s\n", pthid, (char *)retval);

	return 0;
}

  编译上述程序并运行,程序执行的效果如下图3.3所示。

图3.3 程序执行效果图

四、线程和进程原语比较

  纵观进程和线程的区别与联系,其实线程函数和进程函数之间有诸多的相似之处。但是同时又有一些区别,同时实现的方式也不尽相同。下表我们简单对一下线程和进程之间的比较。

表4.1 进程和线程原语的比较

进程原语线程原语原语描述
forkpthread_create创建新的控制流
exitpthread_exit 从现有的控制流中退出
waitpidpthread_join从控制流中得到退出状态
atexitpthread_cancel_push注册在退出控制流时调用的函数
getpidpthread_self 获取控制流的ID
abortpthread_cancel请求控制流的非正常退出

  
  好啦,废话不多说,总结写作不易,如果你喜欢这篇文章或者对你有用,请动动你发财的小手手帮忙点个赞,当然 关注一波 那就更好了,就到这儿了,么么哒(*  ̄3)(ε ̄ *)。

上一篇:Linux – 多线程编程之 - 基础实现一
下一篇:Linux – 本文

  • 19
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

青椒*^_^*凤爪爪

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值