线程(二) 线程清理和控制、线程的属性

线程

线程清理和控制函数

#include <pthread.h>

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

//功能:在线程中注册一个清理处理程序,以便在线程退出时执行特定的清理任务
//参数1:函数指针,指向要执行的线程清理函数。是一个无返回值,参数为void*的函数
//参数2:线程传给清理函数的参数
//返回值:无返回值
#include <pthread.h>

void pthread_cleanup_pop(int execute);

//功能:撤销之前使用pthread_cleanup_push注册的清理处理程序,并根据参数execute决定是否去执行清理函数
//参数:execute=1,执行处理函数,否则不执行光把处理函数弹出栈
//返回值:无返回值
  • 触发线程调用清理函数的动作
    • 主动退出:调用pthread_exit函数退出
    • 被动推出:在同一线程中被其他线程调用pthread_cancel取消
    • 使用非零execute参数调用pthread_cleanup_pop函数

线程清理函数和之前的进程终止函数atexit()类似,通过atexit函数向内核登记多个退出处理函数,当进程结束前就会按照它们被注册的相反顺序去调用。而这里的线程清理函数和它的原理类似,通过pthread_cleanup_push函数将用户自己编写的线程清理函数压入对应的线程的栈区中,然后通过pthread_cleanup_pop函数根据参数execute参数的值将清理函数从线程的栈区弹出并判断是否要去执行这个线程清理函数。通过在退出程序之前去执行用户自己定义的一些清理函数,将一些资源释放(例如将打开的文件关闭,将在堆区开辟出来的空间释放)。通过线程清理函数不仅能够将资源释放,也能减少代码的冗余。如果在多个线程里需要释放的资源是相同的,那么直接在线程结束前去调用系统提供的函数就可以实现资源的释放。

示例–使用线程清理函数和控制函数
#include "header.h"

void cleanup_handler(void *arg)
{
	char *str = (char*)arg;

	printf("clean func: %s\n",str);
}

void* exec_func(void *arg)
{
	int execute = *(int *)arg;

	pthread_cleanup_push(cleanup_handler,"first execute...");		//将线程清理函数压入栈中
	pthread_cleanup_push(cleanup_handler,"second execute...");

	printf("thread is running\n");

	pthread_cleanup_pop(execute);			//将线程清理函数弹出栈中,并根据execute的值判断是否执行线程清理函数
	pthread_cleanup_pop(execute);

	return NULL;
}

int main(void)
{
	int err = -1;
	int execute1 = 1;
	int execute2 = 0;

	pthread_t tid1, tid2;
	
	if((err = pthread_create(&tid1, NULL, exec_func, (void*)&execute1)) != 0)
	{
		perror("pthread_create error");
		exit(EXIT_FAILURE);
	}

	pthread_join(tid1, NULL);

	if((err = pthread_create(&tid2, NULL, exec_func, (void*)&execute2)) != 0)
	{
		perror("pthread_create error");
		exit(EXIT_FAILURE);
	}

	pthread_join(tid2, NULL);

	return 0;
}

image-20240916093639830

通过编译执行可以发现线程清理函数的执行顺序和栈的规则是一样的,先压入的后执行,后压入的先执行,而且这里的线程2的execute = 0,所以即便在程序里向线程压入了线程清理函数,但是没有执行,可见线程清理函数是根据execute的值来决定是否去执行的。这里还有一点需要注意:pthread_cleanup_push()pthread_cleanup_pop()这两个函数是成对出现的,如果不成对出现编译的时候会报错。

示例–使用线程清理函数释放资源
#include "header.h"

typedef struct
{
	int fd;
	int execute;
	char str[48];
}ARG;

//将子线程操作的文件描述符关闭
void cleanup_handler1(void *arg)
{
	int fd = *((int *)arg);
	
	if(close(fd) == 0)
		printf("cleanup_handler: successfully close fd...\n");
}

//将子线程使用malloc函数开辟出来的空间释放并打印调试信息
void cleanup_handler2(void *arg)
{
	char *retval = (char *)arg;

	if(retval != NULL)		//判断不为空说明接收到了来自子线程的调试信息
	{
		printf("debug info:%s\n",retval);
		free(retval);
		retval = NULL;
	}
}

void* exec_func(void *arg)
{
	ARG *r = (ARG*)arg;
	ssize_t nbytes = strlen(r->str);
	char *err_msg = "failed write the message to file";
	char *suc_msg = "successfully write the message to file";
	char *retval = (char *)malloc(sizeof(char) * (strlen(suc_msg) + 1));		//在堆上开辟空间,防止线程资源释放导致数据无效
	if(retval == NULL)
	{
		perror("malloc error");
		exit(EXIT_FAILURE);
	}

	pthread_cleanup_push(cleanup_handler1,(void*)&(r->fd));		//将清理函数压入当前的栈中,后边根据pop函数中execute的值决定是否调用清理函数

	//子线程向文件写入数据
	if(write(r->fd,r->str,nbytes) != nbytes)
	{
		memset(retval,'\0',strlen(retval));
		strcpy(retval,err_msg);
		perror("write error");
		exit(EXIT_FAILURE);
	}
	else
	{
		//如果写入成功就给主线程返回成功写入的调试信息
		memset(retval,'\0',strlen(retval));
		strcpy(retval,suc_msg);
	}

	pthread_cleanup_pop(r->execute);
	
	//子线程给主线程返回调试信息
	pthread_exit((void*)retval);
}

int main(int argc, char **argv)
{
	if(argc < 3)
	{
		fprintf(stderr,"usage: %s [filename] [string]\n",argv[0]);
		exit(EXIT_FAILURE);
	}

	int err = -1;
	int fd;
	char *retval = NULL;
	pthread_t tid;
	ARG arg;

	fd = open(argv[1], O_WRONLY | O_CREAT | O_TRUNC, S_IRWXU | S_IRWXG);	
	//打开外部传入的文件名称,如果没有就创建,拥有者和同组人有可读可写可执行的权限
	if(fd == -1)
	{
		perror("open file error");
		exit(EXIT_FAILURE);
	}

	//将主线程打开的文件,从外部获取的字符串传给子线程
	arg.fd = fd;
	arg.execute = 1;		//pthread_cleanup_pop函数会根据execute判断是否要执行线程清理函数
	strcpy(arg.str,argv[2]);

	if((err = pthread_create(&tid, NULL, exec_func, (void*)&arg)) != 0)
	{
		perror("pthread_create error");
		exit(EXIT_FAILURE);
	}

	pthread_join(tid, (void **)&retval);			//等待子线程退出并回收其资源

	//这里需要注意的是这个函数调用的位置,因为retval是只有等子线程返回以后才有内容
	pthread_cleanup_push(cleanup_handler2,(void *)retval);	
  //向主线程注册信号处理函数,并将来自子线程的调试信息传给线程清理函数,在线程清理函数中去释放空间,防止内存泄漏

	pthread_cleanup_pop(1);		//将清理函数从主线程的栈区弹出并执行线程清理函数
								
	return 0;
}

image-20240916094719380

进程、线程启动和终止方式比较

进程线程
创建fork(创建子进程)pthread_create(创建子线程)
退出return exit _exit _Exitreturn pthread_exit
回收资源wait(防止子进程变成僵尸进程)pthread_join(防止子线程变成僵尸线程)
终止atexit(向内核登记清理函数)pthread_cleanup_push(将线程清理函数压入对应的线程栈中)pthread_cleanup_pop(将线程清理函数从线程的栈区中弹出并根据execute判断是否执行线程清理函数)

线程属性初始化和销毁

前边使用的pthread_create()函数的第二个参数是线程的属性,之前都设置为NULL,表示使用默认属性。现在对线程属性进一步的了解:线程属性在Linux系统中存放在一个pthread_attr_t类型的结构体中,具体如下:

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;

上边线程的属性最常用的是线程的分离属性,当通过代码去设置线程属性的时候,要先对线程属性进行初始化,当程序结束前还要对线程属性进行销毁

#include <pthread.h>

int pthread_attr_init(pthread_attr_t *attr);

//功能:对线程属性进行初始化
//参数:指向线程属性的指针
//成功执行返回0,否则返回错误编码
#include <pthread.h>

int pthread_attr_destroy(pthread_attr_t *attr);

//功能:对线程属性进行销毁
//参数:指向线程属性的指针
//成功执行返回0,否则返回错误编码

设置和获得分离属性

前边有讲过当线程退出的时候,线程的栈区等资源不会释放,必须在主线程中调用pthread_join函数来等待子线程退出并回收子线程的资源,同时调用pthread_join函数的主线程自己也会被阻塞。在线程属性中有一个属性可以将创建的线程设置为分离线程,在线程结束后它的资源会自己释放,而不需要主线程去调用pthread_join函数去回收子线程的资源,并且主线程也不用将自己阻塞, 可以做自己的事情。那么就是通过设置上边线程属性中的detachstate这个参数,通过这个参数在创建的时候就设置子进程为分离线程,让它脱离主线程去运行,在它自己的事务执行完毕后释放其所占用的资源。

#include <pthread.h>

int pthread_attr_getdetachstate(pthread_attr_t *attr, int *detachstate);

//功能:获取线程属性对象中的分离状态。分离状态决定了线程结束后是否需要其他线程对其进行回收
//参数1:指向线程属性的指针
//参数2:指向一个整型变量的指针,当获取到状态后存储到detachstate所指向的空间中
//返回值:成功执行返回0,否则返回错误编码
#include <pthread.h>

int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);

//功能:用于设置线程属性对象中的分离状态
//参数1:指向线程属性的指针
//参数2:要设置的分离状态,可以是以下两个值之一:
					//PTHREAD_CREATE_JOINABLE(默认方式):线程结束后需要其他线程调用pthread_join来回收资源。
					//PTHREAD_CREATE_DETACHED:线程结束后自动释放资源,不需要其他线程进行回收。

这里有几点要注意:

  • 以默认方式启动的线程,在线程结束后不会自动释放资源,需要主线程中调用pthraed_join()函数后才会释放,并且在这期间主线程一直被阻塞。
  • 以分离状态启动的线程,在线程结束后会自动释放所占有的系统资源,不需要主线程调用pthread_join()函数释放,因此主线程不会被阻塞,但如果主线程对分离线程使用这个会直接报错,显示非法的参数。
示例–对比正常启动的线程和分离线程
  • 获取线程状态和设置线程状态

    #include "header.h"
    
    void get_stat(pthread_attr_t *attr)
    {
    	int state;
    
    	if(pthread_attr_getdetachstate(attr, &state) != 0)		//获取线程的分离属性,然后将属性存储在state所指向的空间里
    	{
    		perror("pthread_attr_getdetachstate error");
    	}
    	else
    	{
    		if(state == PTHREAD_CREATE_JOINABLE)
    			printf("joinable thread\n");
    		else if(state == PTHREAD_CREATE_DETACHED)
    			printf("detached thread\n");
    		else
    			printf("error type\n");
    	}
    }
    
    void set_stat(pthread_attr_t *attr, int detachstate)
    {
    	if(pthread_attr_setdetachstate(attr, detachstate) != 0)
    	{
    		perror("pthread_attr_setdetachstate error");
    	}
    }
    
    

    image-20240916202927244

    这里先将这个源文件编译成一个目标文件,然后以目标文件为依赖创建动态库,供后边代码使用

  • 正常启动的线程

    #include "header.h"
    
    void exec_func(void *arg)
    {
    	int cnt = *(int *)arg;
    	int i;
    	
    	for(i=0;i<=cnt;i++)
    	{
    		printf("[thread id:%lx] i = %d\n",pthread_self(),i);
    		sleep(1);
    	}
    	printf("[thread id:%lx] finished!\n",pthread_self());
    	pthread_exit(NULL);
    }
    
    int main(void)
    {
    	int err = -1;
    	int cnt = 8;
    	pthread_t default_tid;
    	pthread_attr_t attr;
    
    	pthread_attr_init(&attr);		//初始化线程属性
    
    	//设置正常启动线程(默认),退出时需要主线程来回收子线程的资源	
    	if((err = pthread_create(&default_tid, NULL, (void*)exec_func, (void*)&cnt)) != 0)
    	{
    		perror("pthread_create error");
    		exit(EXIT_FAILURE);
    	}
    	
    	get_stat(&attr);		//将线程的分离属性打印出来查看
    
    	if((err = pthread_join(default_tid, NULL)) != 0)
    	{
    		fprintf(stderr,"%s\n",strerror(err));
    		exit(EXIT_FAILURE);
    	}
    	else
    	{
    		exec_func((void*)&cnt);			//主线程调用pthread_join函数被阻塞,只有当子线程退出主线程才会继续执行
    	}
    	
    	pthread_attr_destroy(&attr);		//销毁线程属性
    
    	return 0;
    }
    

    image-20240916203433851

  • 设置为分离属性启动的线程

    #include "header.h"
    
    typedef struct
    {
    	int cnt;
    	int time;
    }ARG;
    
    void exec_func(void *arg)
    {
    	ARG *r = (ARG*)arg;
    	int i;
    	
    	for(i=0;i<=r->cnt;i++)
    	{
    		printf("[thread id:%lx] i = %d\n",pthread_self(),i);
    		usleep(r->time);
    	}
    	printf("[thread id:%lx] finished\n",pthread_self());
    
    	pthread_exit(NULL);
    }
    
    int main(void)
    {
    	int err = -1;
    	ARG arg_c;
    	ARG arg_m;
    	pthread_t detach_tid;
    	pthread_attr_t attr;
    
    	srand48(time(NULL));		//设置随机数种子
    
    	arg_c.cnt = 9;
    	arg_c.time = drand48() * 1000000;
    	arg_m.cnt = 9;
    	arg_m.time = drand48() * 500000;
     
    	pthread_attr_init(&attr);	//初始化线程属性
    	
    	set_stat(&attr, PTHREAD_CREATE_DETACHED);	//设置线程为分离线程
    
    	printf("main control thread id is %lx\n",pthread_self());
    	
    	if((err = pthread_create(&detach_tid, &attr, (void*)exec_func, (void*)&arg_c)) != 0)
    	{
    		perror("pthread_create error");
    		exit(EXIT_FAILURE);
    	}
    	
    	get_stat(&attr);		//获取线程属性
    
    	/*if((err = pthread_join(detach_tid, NULL)) != 0)
    	{
    		fprintf(stderr, "%s|%s|%d|%s\n",__FILE__,__func__,__LINE__,strerror(err));
    	}*/
    
    	exec_func((void*)&arg_m);		//主线程也调用此函数,当子线程被设置为分离线程后,主线程也会运行,可以看到两个线程交替运行的情况
    
    	pthread_attr_destroy(&attr);		//销毁线程属性
    
    	return 0;
    }
    
    

    image-20240916211608326

    由于代码用到了上边的get_statset_stat函数,所以涉及到动态库的链接,但是创建好动态库以后如果直接使用是会提示找不到的,需要设置LD_LIBRARY_PATH=库路径环境变量或者将动态库拷贝到/usr/lib目录下去,因为它会在系统默认的库路径(/usr/lib)中找库文件。这里直接使用-Wl,-rpath,库路径进行编译(-Wl,-rpath,库路径 是一个链接器选项,用于在运行时指定动态库的搜索路径。这里的 -Wl 表示传递给链接器的选项,-rpath 是设置运行时库搜索路径的选项,后面跟着的 库路径 是实际的动态库所在路径。这样,在程序运行时,系统会在指定的路径下查找所需的动态库),具体有关静态库和动态库的使用查看静态库和动态库

    通过编译执行可以发现它出错了,原因是调用了pthread_join()函数,显示非法的参数,所以当线程被设置为分离线程后,如果再对它使用pthread_join()函数会报错。

    image-20240917101349820

    将上边的pthread_join()函数注释重新编译执行可以发现子线程的状态为分离线程并且主线程和子线程在交替运行,并没有像上边的正常线程那样子阻塞,原因是分离线程在执行完毕后会自己释放其资源,并不需要主线程调用pthread_join()函数去释放,而正常调用的线程由于并不会自己释放资源,所以需要调用pthread_join()函数等待子进程退出并回收其资源,但同时也会将主线程阻塞。通过对比可以发现分离线程并不能将其返回值传给主线程,而正常启动的线程可以通过pthread_join()函数的第二个参数来接收子线程的返回值,所以它们两个的使用场景需要根据特定的情况去使用。

分离线程的应用场景

通过上边的代码可以发现当线程被设置为分离属性的时候,子线程可以自己执行完代码最后将自己的资源释放。这样就有一个应用场景,主线程给子线程派发一个比较耗时的任务,让子线程去执行完后自己释放自己的资源,而主线程不必调用pthread_join()函数将自己阻塞,自己可以做自己的事情,如此下来就能提高系统处理问题的能力。例如在后边的网络编程中当服务器去接收到很多客户端的请求时就可以将线程属性设置为分离线程,让它在执行完自己的请求后就自己释放它所占有的资源,不必通过主线程来释放子线程的资源来提高系统处理客户端请求的实时性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

日落星野

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

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

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

打赏作者

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

抵扣说明:

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

余额充值