Linux 多线程编程详解

目录

为什么要使用多线程

线程概念

线程的标识 pthread_t

线程的创建

向线程传入参数

线程的退出与回收

线程主动退出

线程被动退出

线程资源回收(阻塞方式)

线程资源回收(非阻塞方式)


为什么要使用多线程

在编写代码时,是否会遇到以下的场景会感觉到难以下手?

要做 2 件事,一件需要阻塞等待,另一件需要实时进行。例如播放器:一边 在屏幕上播放视频,一边在等待用户的按键操作。如果使用单线程的话,程序必 须一会查询有无按键,一会播放视频。查询按键太久,就会导致视频播放卡顿; 视频播放太久,就无法及时响应用户的操作。并且查询按键和播放视频的代码混杂在一起,代码丑陋。

如果使用多线程,线程 1 单独处理按键,线程 2 单独处理播放,可以完美解决上述问题

线程概念

所谓线程,就是操作系统所能调度的最小单位。

普通的进程,只有一个线程在执行对应的逻辑。我们可以通过多线程编程,使一个进程可以去执行多个不同的任务。相比多进程编程而言,线程享有共享资源,即在进程中出现的全局变量, 每个线程都可以去访问它,与进程共享“4G”内存空间,使得系统资源消耗减少。 本章节来讨论 Linux 下 POSIX 线程。

线程的标识 pthread_t

对于进程而言,每一个进程都有一个唯一对应的 PID 号来表示该进程

而对于线程而言,也有一个“类似于进程的 PID 号”,名为 tid,其本质是一个 pthread_t 类型的变量。线程号与进程号是表示线程和进程的唯一标识,但是对于线程号而言,其仅仅在其所属的进程上下文中才有意义。

获取线程号

#include <pthread.h>

pthread_t pthread_self(void);

成功:返回线程号

在程序中,可以通过函数 pthread_self,来返回当前线程的线程号,例程 1 给出了打印线程 tid 号。

测试例程 1:(Phtread_txex1.c)

#include <pthread.h>
#include <stdio.h>

int main()
{
	pthread_t tid = pthread_self();
	printf("tid = %lu\n",(unsigned long)tid);
	return 0;
}

因采用 POSIX 线程接口,故在要编译的时候包含 pthread 库

使用 gcc 编译应 gcc xxx.c -lpthread 方可编译多线程程序

编译结果:

线程的创建

怎么创建线程呢?

使用 pthread_create 函数:

创建线程

#include <pthread.h>

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);

  • 第一个参数为 pthread_t 指针,用来保存新建线程的线程号;
  • 第二个参数表示了线程的属性,一般传入 NULL 表示默认属性;
  • 第三个参数是一个函数指针,就是线程执行的函数。这个函数返回值为 void*, 形参为 void*。
  • 第四个参数则表示为向线程处理函数传入的参数,若不传入,可用 NULL 填充, 有关线程传参后续小节会有详细的说明,接下来通过一个简单例程来使用该函数创建出一个线程。

测试例程 2:(Phtread_txex2.c)

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>

void *fun(void *arg)
{
	printf("pthread_New = %lu\n",(unsigned long)pthread_self());
}

int main()
{

	pthread_t tid1;
	int ret = pthread_create(&tid1,NULL,fun,NULL);
	if(ret != 0){
		perror("pthread_create");
		return -1;
	}

	/*tid_main 为通过pthread_self获取的线程ID,tid_new通过执行pthread_create成功后tid指向的空间*/
	printf("tid_main = %lu tid_new = %lu \n",(unsigned long)pthread_self(),(unsigned long)tid1);
	
	/*因线程执行顺序随机,不加sleep可能导致猪线程先执行,导致进程结束,无法执行到子线程*/
	sleep(1);

	return 0;
}

运行结果:

通过 pthread_create 确 实 可 以 创 建 出 来 线 程 , 主 线 程 中 执 行 pthread_create 后 的 tid 指向了线程号空间,与子线程通过函数 pthread_self 打印出来的线程号一致。

特别说明的是,当主线程伴随进程结束时,所创建出来的线程也会立即结束, 不会继续执行。并且创建出来的线程的执行顺序是随机竞争的,并不能保证哪一 个线程会先运行。可以将上述代码中 sleep 函数进行注释,观察实验现象。

上述运行代码 3 次,其中有 2 次被进程结束,无法执行到子线程的逻辑,最后一 次则执行到了子线程逻辑后结束的进程。如此可以说明,线程的执行顺序不受控制,且整个进程结束后所产生的线程也随之被释放,在后续内容中将会描述如何 控制线程执行。

向线程传入参数

pthread_create()的最后一个参数的为 void*类型的数据,表示可以向线 程传递一个 void*数据类型的参数,线程的回调函数中可以获取该参数,例程 3 举例了如何向线程传入变量地址与变量值。

测试例程 3:(Phtread_txex3.c)

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>

void *fun1(void *arg)
{
	printf("%s:arg = %d Addr = %p\n",__FUNCTION__,*(int *)arg,arg);
}

void *fun2(void *arg)
{
	printf("%s:arg = %d Addr = %p\n",__FUNCTION__,(int)(long)arg,arg);
}

int main()
{

	pthread_t tid1,tid2;
	int a = 50;
	int ret = pthread_create(&tid1,NULL,fun1,(void *)&a); // 21 创建线程传入变量 a 的地址
	if(ret != 0){
		perror("pthread_create");
		return -1;
	}
	ret = pthread_create(&tid2,NULL,fun2,(void *)(long)a); // 27 创建线程传入变量 a 的值
	if(ret != 0){
		perror("pthread_create");
		return -1;
	}
	sleep(1);
	printf("%s:a = %d Add = %p \n",__FUNCTION__,a,&a);
	return 0;
}

运行结果:

例程展示了如何利用线程创建函数的第四个参数向线程传入数据,举例了如何以地址的方式传入值、以变量的方式传入值,

例程代码的 21 行,是将变量 a 先行取地址后,再次强制类型转化为 void*后传入线程,线程处理的回调函数 中,先将万能指针 void*转化为 int*,再次取地址就可以获得该地址变量的值, 其本质在于地址的传递。

例程代码的 27 行,直接将 int 类型的变量强制转化为 void*进行传递(针对不同位数机器,指针对其字数不同,需要 int 转化为 long 在转指针,否则可能会发生警告),在线程处理回调函数中,直接将 void*数据转 248 / 566 化为 int 类型即可,本质上是在传递变量 a 的值。 上述两种方法均可得到所要的值,但是要注意其本质,一个为地址传递,一 个为值的传递。当变量发生改变时候,传递地址后,该地址所对应的变量也会发 生改变,但传入变量值的时候,即使地址指针所指的变量发生变化,但传入的为 变量值,不会受到指针的指向的影响,实际项目中切记两者之间的区别。具体说明见例程 4。

测试例程 4:(Phtread_txex4.c)

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>

void *fun1(void *arg)
{
	while(1){
	
		printf("%s:arg = %d Addr = %p\n",__FUNCTION__,*(int *)arg,arg);
		sleep(1);
	}
}

void *fun2(void *arg)
{
	while(1){
	
		printf("%s:arg = %d Addr = %p\n",__FUNCTION__,(int)(long)arg,arg);
		sleep(1);
	}
}

int main()
{

	pthread_t tid1,tid2;
	int a = 50;
	int ret = pthread_create(&tid1,NULL,fun1,(void *)&a);
	if(ret != 0){
		perror("pthread_create");
		return -1;
	}
	sleep(1);
	ret = pthread_create(&tid2,NULL,fun2,(void *)(long)a);
	if(ret != 0){
		perror("pthread_create");
		return -1;
	}
	while(1){
		a++;
		sleep(1);
		printf("%s:a = %d Add = %p \n",__FUNCTION__,a,&a);
	}
	return 0;
}

运行结果:

上述例程讲述了如何向线程传递一个参数,在处理实际项目中,往往会遇到 传递多个参数的问题,我们可以通过结构体来进行传递,解决此问题。

测试例程 5:(Phtread_txex5.c)

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>

struct Stu{
	int Id;
	char Name[32];
	float Mark;
};

void *fun1(void *arg)
{
	struct Stu *tmp = (struct Stu *)arg;
	printf("%s:Id = %d Name = %s Mark = %.2f\n",__FUNCTION__,tmp->Id,tmp->Name,tmp->Mark);
	
}

int main()
{

	pthread_t tid1,tid2;
	struct Stu stu;
	stu.Id = 10000;
	strcpy(stu.Name,"ZhangSan");
	stu.Mark = 94.6;

	int ret = pthread_create(&tid1,NULL,fun1,(void *)&stu);
	if(ret != 0){
		perror("pthread_create");
		return -1;
	}
	printf("%s:Id = %d Name = %s Mark = %.2f\n",__FUNCTION__,stu.Id,stu.Name,stu.Mark);
	sleep(1);
	return 0;
}

运行结果:

线程的退出与回收

线程的退出情况有三种:第一种是进程结束,进程中所有的线程也会随之结束。

第二种是通过函数 pthread_exit 来主动的退出线程。

第三种被其他线程调用 pthread_cancel 来被动退出。

当线程结束后,主线程可以通过函数 pthread_join/pthread_tryjoin_np 来回收线程的资源,并且获得线程结束后需要返回的数据。

线程主动退出

pthread_exit 函数原型如下:

线程主动退出

#include <pthread.h>

void pthread_exit(void *retval);

pthread_exit 函数为线程退出函数,在退出时候可以传递一个 void*类型的数据带给主线程,若选择不传出数据,可将参数填充为 NULL。

线程被动退出

pthread_cancel 函数原型如下:

线程被动退出,其他线程使用该函数让另一个线程退出

#include <pthread.h> 

int pthread_cancel(pthread_t thread);

成功:返回 0

该函数传入一个 tid 号,会强制退出该 tid 所指向的线程,若成功执行会返回 0。

线程资源回收(阻塞方式)

thread_join 函数原型如下:

线程资源回收(阻塞)

#include <pthread.h> 

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

该函数为线程回收函数,默认状态为阻塞状态,直到成功回收线程后才返回。第一个参数为要回收线程的 tid 号,第二个参数为线程回收后接受线程传出的数据。

线程资源回收(非阻塞方式)

pthread_tryjoin_np 函数原型如下:

线程资源回收(非阻塞)

#define _GNU_SOURCE

#include <pthread.h> 

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

该函数为非阻塞模式回收函数,通过返回值判断是否回收掉线程,成功回 收则返回 0,其余参数与 pthread_join 一致。

测试例程 6:(Phtread_txex6.c)

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>

void *fun1(void *arg)
{
	static int tmp = 0; // 8 必须要static修饰,否则pthread_join无法获取到正确值
	//int tmp = 0;
	tmp = *(int *)arg;
	tmp+=100;
	printf("%s:Addr = %p tmp = %d\n",__FUNCTION__,&tmp,tmp);
	pthread_exit((void *)&tmp);
}


int main()
{

	pthread_t tid1;
	int a = 50;
	void *Tmp = NULL;
	int ret = pthread_create(&tid1,NULL,fun1,(void *)&a); // 23
	if(ret != 0){
		perror("pthread_create");
		return -1;
	}
	pthread_join(tid1,&Tmp);
	printf("%s:Addr = %p Val = %d\n",__FUNCTION__,Tmp,*(int *)Tmp);
	return 0;
}

运行结果:

上述例程先通过 23 行将变量以地址的形式传入线程,在线程中做出了自加 100 的操作,当线程退出的时候通过线程传参,用 void*类型的数据通过 pthread_join 接 受 。 此 例 程 去 掉 了 之 前 加 入 的 sleep 函 数 , 原 因 是 pthread_join 函数具备阻塞的特性,直至成功收回掉线程后才会冲破阻塞,因 此不需要靠考虑主线程会执行到 30 行结束进程的情况。

特别要说明的是例程第 8 行,当变量从线程传出的时候,需要加 static 修饰,对生命周期做出延续, 否则无法传出正确的变量值。

测试例程 7:(Phtread_txex7.c)

#define _GNU_SOURCE 
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>

void *fun(void *arg)
{
	printf("Pthread:%d Come !\n",(int )(long)arg+1);
	pthread_exit(arg);
}


int main()
{
	int ret,i,flag = 0;
	void *Tmp = NULL;
	pthread_t tid[3];
	for(i = 0;i < 3;i++){
		ret = pthread_create(&tid[i],NULL,fun,(void *)(long)i);
		if(ret != 0){
			perror("pthread_create");
			return -1;
		}
	}
	while(1){
		for(i = 0;i <3;i++){
			if(pthread_tryjoin_np(tid[i],&Tmp) == 0){
				printf("Pthread : %d exit !\n",(int )(long )Tmp+1);
				flag++;	
			}
		}
		if(flag >= 3) break;
	}
	return 0;
}

运行结果:

例程 7 展示了如何使用非阻塞方式来回收线程,此外也展示了多个线程可 以指向同一个回调函数的情况。

例程 6 通过阻塞方式回收线程几乎规定了线程回收的顺序,若最先回收的线程未退出,则一直会被阻塞,导致后续先退出的 线程无法及时的回收。

通过函数 pthread_tryjoin_np,使用非阻塞回收,线程可以根据退出先 后顺序自由的进行资源的回收。

测试例程 8:(Phtread_txex8.c)

#define _GNU_SOURCE 
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>

void *fun1(void *arg)
{
	printf("Pthread:1 come!\n");
	while(1){
		sleep(1);
	}
}

void *fun2(void *arg)
{
	printf("Pthread:2 come!\n");
	pthread_cancel((pthread_t )(long)arg); // 杀死线程 1,使之强制退出
	pthread_exit(NULL);
}

int main()
{
	int ret,i,flag = 0;
	void *Tmp = NULL;
	pthread_t tid[2];
	ret = pthread_create(&tid[0],NULL,fun1,NULL); // 27
	if(ret != 0){
		perror("pthread_create");
		return -1;
	}
	sleep(1);
	ret = pthread_create(&tid[1],NULL,fun2,(void *)tid[0]); // 33 传输线程 1 的线程号

	if(ret != 0){
		perror("pthread_create");
		return -1;
	}
	while(1){ //通过非阻塞方式收回线程,每次成功回收一个线程变量自增,直至 2 个线程全数回收
		for(i = 0;i <2;i++){
			if(pthread_tryjoin_np(tid[i],NULL) == 0){
				printf("Pthread : %d exit !\n",i+1);
				flag++;	
			}
		}
		if(flag >= 2) break;
	}
	return 0;
}

运行结果:

例程 8 展示了如何利用 pthread_cancel 函数主动的将某个线程结束。

27 行与 33 行创建了线程,将第一个线程的线程号传参形式传入了第二个线程。第一个的线程执行死循环睡眠逻辑,理论上除非进程结束,其永远不会结束,但在第二个线程中调用了 pthread_cancel 函数,相当于向该线程发送一个退出的指令,导致线程被退出,最终资源被非阻塞回收掉。

此例程要注意第 32 行的 sleep 函数,一定要确保线程 1 先执行,因线程是无序执行,故加入该睡眠函数控制顺序

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/125335.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

显著提升!| (WOA)融合模拟退火和自适应变异的混沌鲸鱼优化算法应用于函数寻优

鲸鱼优化算法(whale optimization algorithm,WOA)是由Mirjalili和Lewis[1]于2016年提出的一种新型群体智能优化搜索方法,它源于对自然界中座头鲸群体狩猎行为的模拟&#xff0c;与其它群体智能优化算法相比&#xff0c;WOA算法结构新颖, 控制参数少&#xff0c;在许多数值优化和…

在IDEA中配置Web开发环境

一、idea配置Web开发环境 第一步&#xff1a;下载并安装Tomcat服务器&#xff08;建议放根目录&#xff0c;完整路径中不要出现中文&#xff09; 第二步&#xff1a;打开IDEA&#xff0c;新建java项目 第三步&#xff1a;为项目添加Web应用 在项目上右键➡️选择“Add Framew…

在线存储系统源码 网盘网站源码 云盘系统源码

Cloudreve云盘系统源码-支持本地储存和对象储存,界面美观 云盘系统安装教程 测试环境:PHP7.1 MYSQL5.6 Apache 上传源码到根目录 安装程序: 浏览器数据 http://localhost/CloudreveInstallerlocalhost更换成你的网址 安装完毕 记住系统默认的账号密码 温馨提示:如果默认…

已解决:云原生领域的超时挂载Bug — Kubernetes深度剖析

&#x1f337;&#x1f341; 博主猫头虎&#xff08;&#x1f405;&#x1f43e;&#xff09;带您 Go to New World✨&#x1f341; &#x1f984; 博客首页——&#x1f405;&#x1f43e;猫头虎的博客&#x1f390; &#x1f433; 《面试题大全专栏》 &#x1f995; 文章图文…

线性代数(三) | 向量组的秩 线性相关无关 几何直观理解 题解应用

文章目录 1 维数&#xff1f;向量组的秩究竟是什么&#xff1f;1.1 线是一维的1.2 面是二维的1.3 体是三维的 2 线性相关、线性无关、线性表示究竟是什么&#xff1f;2.1 基于以上几何直观的解题角度2.2 基于方程组的解题角度 1 维数&#xff1f;向量组的秩究竟是什么&#xff…

10道高频Vuex面试题快问快答

※其他的快问快答&#xff0c;看这里&#xff01; 10道高频Qiankun微前端面试题快问快答 10道高频webpack面试题快问快答 20道高频CSS面试题快问快答 20道高频JavaScript面试题快问快答 30道高频Vue面试题快问快答 面试中的快问快答 快问快答的情景在面试中非常常见。 在面试过…

[100天算法】-面试题 17.11.单词距离(day 68)

题目描述 有个内含单词的超大文本文件&#xff0c;给定任意两个单词&#xff0c;找出在这个文件中这两个单词的最短距离(相隔单词数)。如果寻找过程在这个文件中会重复多次&#xff0c;而每次寻找的单词不同&#xff0c;你能对此优化吗?示例&#xff1a;输入&#xff1a;word…

天津WEB前端培训哪家好?Web机构推荐!

05年以后&#xff0c;互联网已经进入了web2.0时代&#xff0c;同时也标志着网站的前端由此发生了翻天覆地的变化&#xff0c;现在市场上对WEB前端开发工程师岗位有着很大的需求&#xff0c;学习web前端开发的方式有很多种&#xff0c;对于初学者来说&#xff0c;选择自学还是培…

大数据毕业设计选题推荐-河长制大数据监测平台-Hadoop-Spark-Hive

✨作者主页&#xff1a;IT研究室✨ 个人简介&#xff1a;曾从事计算机专业培训教学&#xff0c;擅长Java、Python、微信小程序、Golang、安卓Android等项目实战。接项目定制开发、代码讲解、答辩教学、文档编写、降重等。 ☑文末获取源码☑ 精彩专栏推荐⬇⬇⬇ Java项目 Python…

2023.11-9 hive数据仓库,概念,架构

目录 一.HDFS、HBase、Hive的区别 二.大数据相关软件 三. Hive 的优缺点 1&#xff09;优点 2&#xff09;缺点 四. Hive 和数据库比较 1&#xff09;查询语言 2&#xff09;数据更新 3&#xff09;执行延迟 4&#xff09;数据规模 五.hive架构流程 六.MetaStore元…

AI:73-结合语法知识的神经机器翻译研究

🚀 本文选自专栏:AI领域专栏 从基础到实践,深入了解算法、案例和最新趋势。无论你是初学者还是经验丰富的数据科学家,通过案例和项目实践,掌握核心概念和实用技能。每篇案例都包含代码实例,详细讲解供大家学习。 📌📌📌在这个漫长的过程,中途遇到了不少问题,但是…

企业微信开发教程一:添加企微应用流程图解以及常见问题图文说明

最近在前辈的基础上新添加了一个企微应用&#xff0c;过程中遇到了一些卡点&#xff0c;这里一一通过图片标注与注释的方式记录一下&#xff0c;希望能给后来人提供一些清晰明了的帮助&#xff0c;话不多说&#xff0c;大家直接看图吧。 &#xff08;文中包括一些本项目独有的配…

[Matlab]基于LSTM+NSGA2的风光火力发电策略优化

最近比较忙&#xff0c;好久没分享案例啦&#xff0c;今天简单分享一个滚动时域的多目标优化 一 模型介绍 1 风电 2 光伏 3 火电 4 储能 5 用电需求 等五个对象。 其中风电和光伏还有用电需求&#xff0c;用历史数据LSTM网络&#xff0c;训练一个预测模型&#xff1b;火电根据策…

使用sizeof()和strlen()去计算【数组】和【指针】的大小

文章目录 一、知识回顾1、回顾sizeof()、strlen的作用&#xff1a;2、数组和指针3、数组名 二、sizeof()、strlen()的使用区别1、注意区别&#xff1a;2、一维数组与一级指针3、二维数组与二级指针 三、总结回顾 一、知识回顾 1、回顾sizeof()、strlen的作用&#xff1a; siz…

LinkedList的插入速度一定比ArrayList快吗?

目录 一、有一道经典的面试题&#xff0c;“ArrayList 和 LinkedList 的区别是什么&#xff1f;”1、小白答法&#xff1a;2、入门答法&#xff1a;3、系统回答 二、LinkedList的插入速度一定比ArrayList快吗&#xff1f;三、分析一下两种数据结构的add源码1、先分析熟悉的Arra…

07【保姆级】-GO语言的程序流程控制【if switch for while 】

之前我学过C、Java、Python语言时总结的经验&#xff1a; 先建立整体框架&#xff0c;然后再去抠细节。先Know how&#xff0c;然后know why。先做出来&#xff0c;然后再去一点点研究&#xff0c;才会事半功倍。适当的囫囵吞枣。因为死抠某个知识点很浪费时间的。对于GO语言&a…

【C++】复杂的多继承及其缺陷(菱形继承)

本篇要分享的内容是C中多继承的缺陷&#xff1a;菱形继承。 以下为本篇目录 目录 1.多继承的缺陷与解决方法 2.虚继承的底层原理 3.虚继承底层原理的设计原因 1.多继承的缺陷与解决方法 首先观察下面的图片判断它是否为多继承 这实际上是一个单继承&#xff0c;单继承的特…

clang插件对llvm源码插桩,分析函数调用日志(2)--google镜像

tick_plot__compile.ipynb clang插件对llvm源码插桩&#xff0c;分析函数调用日志(1) 分析 进出、链、出 df进出df[ df[tickKind].isin( [FuncEnter,FuncReturn] ) ]#代码中&#xff0c;只有在函数进入时&#xff0c;计算了链条长度 并写磁盘 df入df[ df[tickKind].isin…

18 CDN详解

1、理解CDN 1.CDN 和电商系统的分布式仓储系统一样&#xff0c;就近发货给客户(客户端)&#xff0c;所以&#xff0c;必然是提前在仓库中存储了某些商品. 2.CDN最擅长的是缓存静态数据&#xff0c;比如电商系统的热点静态页面&#xff0c;秒杀场景的页面等.问题&#xff1a;向…

tqdm学习

from tqdm import tqdmepochs 10 epoch_bar tqdm(range(epochs)) count 0 for _ in epoch_bar:count count1print("count {}".format(count))print(_)每次就是一个epoch
最新文章