当前位置: 首页 > article >正文

Linux C/C++编程-线程退出时的清理机会

【图书推荐】《Linux C与C++一线开发实践(第2版)》_linux c与c++一线开发实践pdf-CSDN博客
《Linux C与C++一线开发实践(第2版)(Linux技术丛书)》(朱文伟,李建英)【摘要 书评 试读】- 京东图书 (jd.com)

Linux系统与编程技术_夏天又到了的博客-CSDN博客

Linux C/C++编程的线程创建-CSDN博客

前面讲了线程的终止,主动终止可以认为是线程正常终止,这种方式是可预见的。被动终止是其他线程要求其结束,这种退出方式是不可预见的,是一种异常终止。不论是可预见的线程终止还是异常终止,都会存在资源释放的问题,在不考虑因运行出错而退出的前提下,如何保证线程终止时能顺利地释放掉自己所占用的资源,特别是锁资源,就是一个必须解决的问题。经常出现的情形是资源独占锁的使用:线程为了访问临界资源而为其加上锁,但在访问过程中线程被外界取消,如果取消成功了,则该临界资源将永远处于锁定状态得不到释放。外界取消操作是不可预见的,因此的确需要一个机制来简化用于资源释放的编程,也就是需要一个在线程退出时执行清理的机会。关于锁后面会讲到,这里只需要知道谁上了锁,谁就要负责解锁,否则会引起程序死锁。

我们来看一个场景:线程1执行这样一段代码:

void *thread1(void *arg)  
{
	pthread_mutex_lock(&mutex);  	// 上锁
	// 调用某个阻塞函数,比如套接字的accept,该函数等待客户连接
	sock = accept(...);          
	pthread_mutex_unlock(&mutex);
}

这个例子中,如果线程1执行accept,线程就会阻塞(也就是等在那里,有客户端连接的时候才返回,或者出现其他故障)。现在线程1处于等待中,这时线程2发现线程1等了很久,不耐烦了,它想关掉线程1,于是调用pthread_cancel或者类似函数请求线程1立即退出。这时线程1仍然在accept等待中,当它收到线程2的cancel信号后,就会从accept中退出,然后终止线程,但是这个时候线程1还没有执行解锁函数pthread_mutex_unlock(&mutex);,也就是说锁资源没有释放,从而造成其他线程的死锁问题,也就是其他在等待这个锁资源的线程将永远等不到了。因此,必须在线程接收到cancel后,用一种方法来保证异常退出(也就是线程没达到终点)时可以做清理工作(主要是解锁方面)。

对此,POSIX线程库提供了函数pthread_cleanup_push和pthread_cleanup_pop,让线程退出时可以做一些清理工作。这两个函数采用先入后出的栈结构管理,前者会把一个函数压入清理函数栈,后者用来弹出栈顶的清理函数,并根据参数来决定是否执行清理函数。多次调用函数pthread_cleanup_push将把当前在栈顶的清理函数往下压,弹出清理函数时,在栈顶的清理函数先被弹出。栈的特点是先进后出。pthread_cleanup_push声明如下:

void pthread_cleanup_push(void (*routine)(void *), void *arg);

其中,参数routine是一个函数指针,arg是该函数的参数。由pthread_cleanup_push压栈的清理函数在下面3种情况下会执行:

(1)线程主动结束时,比如return或调用pthread_exit的时候。

(2)调用函数pthread_cleanup_pop,且其参数为非0时。

(3)线程被其他线程取消时,也就是有其他的线程对该线程调用pthread_cancel函数。

函数pthread_cleanup_pop声明如下:

void pthread_cleanup_pop(int execute);

其中,参数execute用来决定在弹出栈顶清理函数的同时是否执行清理函数,取0时表示不执行清理函数,非0时则执行清理函数。要注意的是,函数pthread_cleanup_pop与pthread_cleanup_push必须成对出现在同一个函数中,否则就会出现语法错误。

了解这两个函数后,我们可以把上面可能会引起死锁的线程1的代码改写如下:

void *thread1(void *arg)  
{
	pthread_cleanup_push(clean_func,...) 	// 压栈一个清理函数 clean_func
	pthread_mutex_lock(&mutex); 			// 上锁
	// 调用某个阻塞函数,比如套接字的accept,该函数等待客户连接
	sock = accept(...);            

	pthread_mutex_unlock(&mutex);  			// 解锁
	pthread_cleanup_pop(0); 				// 弹出清理函数,但不执行,因为参数是0
	return NULL;
}

在上面的代码中,如果accept被其他线程cancel后退出,就会自动调用clean_func函数,在这个函数中可以释放锁资源。如果accept没有被cancel,那么线程继续执行,当执行到“pthread_mutex_unlock(&mutex);”时,线程自己已正确地释放资源了,再执行到“pthread_cleanup_pop(0);”时,会把前面压栈的清理函数clean_func弹出栈,但不会去执行它(因为参数是0)。现在的流程就安全了。

【例8.17】线程主动终止时,调用清理函数

(1)打开Visual Studio Code,新建一个test.cpp文件,在test.cpp中输入代码:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h> // strerror
 
void mycleanfunc(void *arg) 					// 清理函数
{
	printf("mycleanfunc:%d\n", *((int *)arg));	// 打印传进来的不同参数					 
}
void *thfrunc1(void *arg)
{
	int m=1;
	printf("thfrunc1 comes \n");
	pthread_cleanup_push(mycleanfunc, &m);  // 把清理函数压栈
	return (void *)0;	 	// 退出线程
	pthread_cleanup_pop(0);	// 把清理函数出栈,这句不会执行,但必须有,否则编译不过
}
 
void *thfrunc2(void *arg)
{
	int m = 2;
	printf("thfrunc2 comes \n");
	pthread_cleanup_push(mycleanfunc, &m); // 把清理函数压栈
	pthread_exit(0); // 退出线程
	pthread_cleanup_pop(0); // 把清理函数出栈,这句不会执行,但必须有,否则编译不过	
}

int main(void)
{
	pthread_t pid1,pid2;
	int res;
	res = pthread_create(&pid1, NULL, thfrunc1, NULL); // 创建线程1
	if (res) 
	{
		printf("pthread_create failed: %d\n", strerror(res));
		exit(1);
	}
	pthread_join(pid1, NULL); // 等待线程1结束
	
	res = pthread_create(&pid2, NULL, thfrunc2, NULL); // 创建线程2
	if (res) 
	{
		printf("pthread_create failed: %d\n", strerror(res));
		exit(1);
	}
	pthread_join(pid2, NULL); // 等待线程2结束
	
	printf("main over\n");
	return 0;
}

(2)上传test.cpp到Linux,在终端下输入命令g++ -o test test.cpp -lpthread,其中pthread是线程库的名字,然后运行test,运行结果如下:

[root@localhost cpp98]# g++ -o test test.cpp -lpthread
[root@localhost cpp98]# ./test
thfrunc1 comes 
mycleanfunc:1
thfrunc2 comes 
mycleanfunc:2
main over

从例子中可以看到,无论是return还是pthread_exit都会引起清理函数的执行。值得注意的是,pthread_cleanup_pop必须和pthread_cleanup_push成对出现在同一个函数中,否则编译不过,读者可以把pthread_cleanup_pop注释掉后再编译试试。这个例子是线程主动调用清理函数,下面我们再看一个由pthread_cleanup_pop执行清理函数的例子。

【例8.18】pthread_cleanup_pop调用清理函数

(1)打开Visual Studio Code,新建一个test.cpp文件,在test.cpp中输入代码:

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h> // strerror
 
void mycleanfunc(void *arg)					// 清理函数
{
	printf("mycleanfunc:%d\n", *((int *)arg));						 
}
void *thfrunc1(void *arg) 					// 线程函数
{
	int m=1,n=2;
	printf("thfrunc1 comes \n");
	pthread_cleanup_push(mycleanfunc, &m); 	// 把清理函数压栈
	pthread_cleanup_push(mycleanfunc, &n); 	// 再把一个清理函数压栈
	pthread_cleanup_pop(1);// 出栈清理函数,并执行
	pthread_exit(0); 						// 退出线程
	pthread_cleanup_pop(0); 				// 不会执行,仅为了成对
}
  
int main(void)
{
	pthread_t pid1 ;
	int res;
	res = pthread_create(&pid1, NULL, thfrunc1, NULL); // 创建线程
	if (res) 
	{
		printf("pthread_create failed: %d\n", strerror(res));
		exit(1);
	}
	pthread_join(pid1, NULL);				// 等待线程结束
	
	printf("main over\n");
	return 0;
}

(2)上传test.cpp到Linux,在终端下输入命令g++ -o test test.cpp -lpthread,其中pthread是线程库的名字,然后运行test,运行结果如下:

[root@localhost cpp98]# g++ -o test test.cpp -lpthread
[root@localhost cpp98]# ./test
thfrunc1 comes 
mycleanfunc:2
mycleanfunc:1
main over

从例子中可以看出,我们连续压了两次清理函数入栈,第一次压栈的清理函数在栈底,第二次压栈的清理函数就到栈顶了,出栈的时候应该是第二次压栈的清理函数先执行,因此“pthread_cleanup_pop(1);”执行的是传n进去的清理函数,输出的整数值是2。pthread_exit退出线程时,引发执行的清理函数是传m进去的清理函数,输出的整数值是1。下面再看最后一种情况,线程被取消时引发清理函数。

【例8.19】取消线程时引发清理函数

(1)打开Visual Studio Code,新建一个test.cpp文件,在test.cpp中输入代码:

#include<stdio.h>  
#include<stdlib.h>  
#include <pthread.h>  
#include <unistd.h> // sleep

void mycleanfunc(void *arg) // 清理函数
{
	printf("mycleanfunc:%d\n", *((int *)arg)); 
}
 
void *thfunc(void *arg)  
{  
	int i = 1;  
	printf("thread start-------- \n"); 
	pthread_cleanup_push(mycleanfunc, &i); 	// 把清理函数压栈
	while (1)  
	{
		i++;  
		printf("i=%d\n", i);
	}	
	printf("this line will not run\n"); 	// 这句不会调用
	pthread_cleanup_pop(0);  				// 仅仅为了成对调用
	
	return (void *)0;  
}  
int main()  
{  
	void *ret = NULL;  
	int iret = 0;  
	pthread_t tid;  
	pthread_create(&tid, NULL, thfunc, NULL);	// 创建线程
	sleep(1); 					 // 等待一会,让子线程开始while循环
	pthread_cancel(tid); 		// 发送取消线程的请求  
	pthread_join(tid, &ret);  	// 等待线程结束
	if (ret == PTHREAD_CANCELED) 				// 判断是否成功取消线程
		printf("thread has stopped,and exit code: %d\n", ret);  
		// 打印返回值,应该是-1
	else
		printf("some error occured");
          
	return 0;  
}

(2)上传test.cpp到Linux,在终端下输入命令g++ -o test test.cpp -lpthread,其中pthread是线程库的名字,然后运行test,运行结果如下:

[root@localhost cpp98]# g++ -o test test.cpp -lpthread
[root@localhost cpp98]# ./test
i=2
i=3
i=4
...
i=24383
i=24384
i=24385
i=24386
i=24387
i=24388
i=24389i=24389
mycleanfunc:24389
thread has stopped,and exit code: -1

从这个例子可以看出,子线程在循环打印i的值,一直到被取消。由于循环里有系统调用printf,因此取消成功。取消成功的时候,将会执行清理函数,在清理函数中打印的i值将是执行很多次i++后的i值。这是因为我们压栈清理函数的时候,传给清理函数的是i的地址,而执行清理函数的时候,i的值已经变了,所以打印的是最新的i值。


http://www.kler.cn/a/450337.html

相关文章:

  • 微调大模型时,如何进行数据预处理? 将<input, output>转换为模型所需的<input_ids, labels, attention_mask>
  • OpenAI 普及 ChatGPT,开通热线电话,近屿智能深耕AI培训
  • Rasa框架的优点和缺点
  • 链路聚合与GVRP的混合构建(eNSP)
  • 重温设计模式--外观模式
  • Docker快速入门到项目部署
  • 易语言 OCR 文字识别
  • LightGBM分类算法在医疗数据挖掘中的深度探索与应用创新(上)
  • 数据结构-串-顺序结构实现
  • 如何使用vscode解决git冲突
  • 【微信小程序】微信小程序中的异步函数是如何实现同步功能的
  • C# 异步编程与多线程简析
  • 【python】装饰器
  • 云端地球模型标注如何添加?
  • Rasa框架的优点和缺点
  • EasyExcel 模板+公式填充
  • opencv中的常用的100个API
  • Maven 环境变量 MAVEN_HOME 和 M2_HOME 区别以及 IDEA 修改 Maven repository 路径全局
  • 矩阵:Input-Output Interpretation of Matrices (中英双语)
  • VMware Workstation虚拟机网络模式
  • 32 - Java 8 函数式接口
  • Light | 单点光场多维信息重构
  • 力扣-数据结构-1【算法学习day.72】
  • 【微信小程序】3|首页搜索框 | 我的咖啡店-综合实训
  • Linux——字符设备驱动控制LED
  • 高性能Web网关:OpenResty 基础讲解