正点原子I.MX6U-MINI应用篇9嵌入式Linux中的多线程编程pthread

Posted 果果小师弟

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了正点原子I.MX6U-MINI应用篇9嵌入式Linux中的多线程编程pthread相关的知识,希望对你有一定的参考价值。

一、线程的使用

1.1 为什么要使用多线程

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

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

1.2 线程概念

所谓线程,就是操作系统所能调度的最小单位。普通的进程,只有一个线程在执行对应的逻辑。我们可以通过多线程编程,使一个进程可以去执行多个不同的任务。相比多进程编程而言,线程享有共享资源,即在进程中出现的全局变量,每个线程都可以去访问它,与进程共享“4G”内存空间,使得系统资源消耗减少

1.3 线程的标识pthread_t

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

获取线程号

#include <pthread.h>
pthread_t pthread_self(void);
//成功:返回线程号

在程序中,可以通过函数pthread_self,来返回当前线程的线程号。

测试例程1:(Phtread_txex2.1)

#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方可编译多线程程序。

gcc -o Pthread_Text1 Pthread_Text1.c -lpthread

编译结果:

1.4 线程的创建

怎么创建线程呢?使用pthread_create函数:

//创建线程
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routi
ne) (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;

注意:因采用POSIX线程接口,故在要编译的时候包含pthread库,使用gcc编译应gcc xxx.c -lpthread方可编译多线程程序。

gcc -o Pthread_Text2 Pthread_Text2.c -lpthread

编译结果:

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

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

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

1.5 向线程传入参数

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);//地址传递
	if(ret != 0)
		perror("pthread_create");
		return -1;
	
	ret = pthread_create(&tid2,NULL,fun2,(void *)(long)a);//值传递
	if(ret != 0)
		perror("pthread_create");
		return -1;
	
	sleep(1);
	printf("%s:a = %d Add = %p \\n",__FUNCTION__,a,&a);
	return 0;

注意:因采用POSIX线程接口,故在要编译的时候包含pthread库,使用gcc编译应gcc xxx.c -lpthread方可编译多线程程序。

gcc -o Pthread_Text3 Pthread_Text3.c -lpthread

编译结果:


本例程展示了如何利用线程创建函数的第四个参数向线程传入数据,举例了如何以地址的方式传入值、以变量的方式传入值,例程代码的21行,是将变量a先行取地址后,再次强制类型转化为 void后传入线程,线程处理的回调函数中,先将万能指针 void转化为 int*,再次取地址就可以获得该地址变量的值,其本质在于地址的传递。例程代码的 27 行,直接将 int 类型的变量强制转化为void进行传递(针对不同位数机器,指针对其字数不同,需要 int 转化为 long在转指针,否则可能会发生警告),在线程处理回调函数中,直接将 void数据转化为 int 类型即可,本质上是在传递变量a的值

上述两种方法均可得到所要的值,但是要注意其本质,一个为地址传递,一个为值的传递当变量发生改变时候,传递地址后,该地址所对应的变量也会发生改变,但传入变量值的时候,即使地址指针所指的变量发生变化,但传入的为变量值,不会受到指针的指向的影响,实际项目中切记两者之间的区别

下面的例程就说明了地址传递和值传递的区别,请注意!

测试例程 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;

注意:因采用POSIX线程接口,故在要编译的时候包含pthread库,使用gcc编译应gcc xxx.c -lpthread方可编译多线程程序。

gcc -o Pthread_Text4 Pthread_Text4.c -lpthread

编译结果:

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

测试例程 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;

注意:因采用POSIX线程接口,故在要编译的时候包含pthread库,使用gcc编译应gcc xxx.c -lpthread方可编译多线程程序。

gcc -o Pthread_Text5 Pthread_Text5.c -lpthread

编译结果:

1.6 线程的退出与回收

线程的退出情况有三种:

  • 第一种是进程结束,进程中所有的线程也会随之结束。

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

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

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

1.6.1 线程主动退出

pthread_exit函数原型如下:

//线程主动退出
#include <pthread.h>
void pthread_exit(void *retval);

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

1.6.2 线程被动退出

pthread_cancel函数原型如下:

//线程被动退出,其他线程使用该函数让另一个线程退出
#include <pthread.h>
int pthread_cancel(pthread_t thread);
//成功:返回 0

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

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

pthread_join函数原型如下:

//线程资源回收(阻塞)
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);

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

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

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;//必须要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);
	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;

注意:因采用POSIX线程接口,故在要编译的时候包含pthread库,使用gcc编译应gcc xxx.c -lpthread方可编译多线程程序。

gcc -o Pthread_Text6 Pthread_Text6.c -lpthread

编译结果:

上述例程先通过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;

注意:因采用POSIX线程接口,故在要编译的时候包含pthread库,使用gcc编译应gcc xxx.c -lpthread方可编译多线程程序。

gcc -o Pthread_Text7 Pthread_Text7.c -lpthread

编译结果:


例程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);
	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);
	if(ret != 0)
		perror("pthread_create");
		return -1;
	
	sleep(1);
	ret = pthread_create(&tid[1],NULL,fun2,(正点原子I.MX6U-MINI应用篇8嵌入式Linux网络通信socket编程

正点原子I.MX6U-MINI应用篇5嵌入式Linux在LCD上显示BMPJPGPNG图片

正点原子I.MX6U-MINI应用篇6嵌入式Linux在LCD屏幕上显示字符

正点原子I.MX6U-MINI驱动篇1字符设备驱动开发-Hello驱动(不涉及硬件操作)

正点原子I.MX6U-MINI驱动篇2嵌入式 Linux驱动开发之点灯大法

正点原子I.MX6U-MINI应用篇1编写第一个应用App程序helloworld