嵌入式linux/鸿蒙开发板(IMX6ULL)开发(十七)多线程编程(上)

简介: 嵌入式linux/鸿蒙开发板(IMX6ULL)开发(十七)多线程编程

1.多线程编程


本章将分为两大部分进行讲解,第一部分将引出线程的使用场景及基本概念,通过示例代码来说明一个线程创建到退出到回收的基本流程。第二部分则会通过示例代码来说明如果控制好线程,从临界资源访问与线程的执行顺序控制上引出互斥锁、信号量的概念与使用方法。


1.1 线程的使用


1.1.1 为什么要使用多线程


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

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

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

1670913021985.jpg


1.1.2 线程概念


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


1.1.3 线程的标识pthread_t


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


获取线程号

#include <pthread.h>
pthread_t pthread_self(void);


成功:返回线程号

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

使用GIT下载所有源码后,本节源码位于如下目录:

01_all_series_quickstart\
04_嵌入式Linux应用开发基础知识\source\13_thread\01_文档配套源码
Pthread_Text1.c


测试例程1:(Phtread_txex1.c)

1 #include <pthread.h>
2 #include <stdio.h>
3
4 int main()
5 {
6  pthread_t tid = pthread_self();//获取主线程的tid号
7  printf("tid = %lu\n",(unsigned long)tid);
8       return 0;
9 }


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


编译结果:

1670913069163.jpg


1.1.4 线程的创建


怎么创建线程呢?使用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填充,有关线程传参后续小节会有详细的说明,接下来通过一个简单例程来使用该函数创建出一个线程。


使用GIT下载所有源码后,本节源码位于如下目录:

01_all_series_quickstart\
04_嵌入式Linux应用开发基础知识\source\13_thread\01_文档配套源码
Pthread_Text2.c


测试例程2:(Phtread_txex2.c)

1  #include <pthread.h>
2  #include <stdio.h>
3  #include <unistd.h>
4  #include <errno.h>
5 
6  void *fun(void *arg)
7  {
8   printf("pthread_New = %lu\n",(unsigned long)pthread_self());//打印线程的tid号
9  }
10
11  int main()
12  {
13
14  pthread_t tid1;
15  int ret = pthread_create(&tid1,NULL,fun,NULL);//创建线程
16  if(ret != 0){
17    perror("pthread_create");
18    return -1;
19  }
20
21  /*tid_main 为通过pthread_self获取的线程ID,tid_new通过执行pthread_create成功后tid指向的空间*/
22  printf("tid_main = %lu tid_new = %lu \n",(unsigned long)pthread_self(),(unsigned long)tid1);
23  
24  /*因线程执行顺序随机,不加sleep可能导致主线程先执行,导致进程结束,无法执行到子线程*/
25  sleep(1);
26
27  return 0;
28  }
29


运行结果:

1670913122199.jpg


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

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


去掉上述代码25行后运行结果:

1670913130277.jpg

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


1.1.5 向线程传入参数


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


使用GIT下载所有源码后,本节源码位于如下目录:

01_all_series_quickstart\
04_嵌入式Linux应用开发基础知识\source\13_thread\01_文档配套源码
Pthread_Text3.c


测试例程3:(Phtread_txex3.c)

1  #include <pthread.h>
2  #include <stdio.h>
3  #include <unistd.h>
4  #include <errno.h>
5 
6  void *fun1(void *arg)
7  {
8   printf("%s:arg = %d Addr = %p\n",__FUNCTION__,*(int *)arg,arg);
9  }
10
11  void *fun2(void *arg)
12  {
13  printf("%s:arg = %d Addr = %p\n",__FUNCTION__,(int)(long)arg,arg);
14  }
15
16  int main()
17  {
18
19  pthread_t tid1,tid2;
20  int a = 50;
21  int ret = pthread_create(&tid1,NULL,fun1,(void *)&a);//创建线程传入变量a的地址
22  if(ret != 0){
23    perror("pthread_create");
24    return -1;
25  }
27  ret = pthread_create(&tid2,NULL,fun2,(void *)(long)a);//创建线程传入变量a的值
28  if(ret != 0){
29    perror("pthread_create");
30    return -1;
31  }
32  sleep(1);
33  printf("%s:a = %d Add = %p \n",__FUNCTION__,a,&a);
34  return 0;
35  }
36


运行结果:

1670913169804.jpg


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

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


使用GIT下载所有源码后,本节源码位于如下目录:

01_all_series_quickstart\
04_嵌入式Linux应用开发基础知识\source\13_thread\01_文档配套源码
Pthread_Text4.c


测试例程4:(Phtread_txex4.c)

1  #include <pthread.h>
2  #include <stdio.h>
3  #include <unistd.h>
4  #include <errno.h>
5 
6  void *fun1(void *arg)
7  {
8   while(1){
9   
10    printf("%s:arg = %d Addr = %p\n",__FUNCTION__,*(int *)arg,arg);
11    sleep(1);
12  }
13  }
14
15  void *fun2(void *arg)
16  {
17  while(1){
18  
19    printf("%s:arg = %d Addr = %p\n",__FUNCTION__,(int)(long)arg,arg);
20    sleep(1);
21  }
22  }
23
24  int main()
25  {
26
27  pthread_t tid1,tid2;
28  int a = 50;
29  int ret = pthread_create(&tid1,NULL,fun1,(void *)&a);
30  if(ret != 0){
31    perror("pthread_create");
32    return -1;
33  }
34  sleep(1);
35  ret = pthread_create(&tid2,NULL,fun2,(void *)(long)a);
36  if(ret != 0){
37    perror("pthread_create");
38    return -1;
39  }
40  while(1){
41    a++;
42    sleep(1);
43    printf("%s:a = %d Add = %p \n",__FUNCTION__,a,&a);
44  }
45  return 0;
46  }
47


运行结果:

1670913203131.jpg

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


使用GIT下载所有源码后,本节源码位于如下目录:

01_all_series_quickstart\
04_嵌入式Linux应用开发基础知识\source\13_thread\01_文档配套源码
Pthread_Text5.c


测试例程5:(Phtread_txex5.c)

1  #include <pthread.h>
2  #include <stdio.h>
3  #include <unistd.h>
4  #include <string.h>
5  #include <errno.h>
6 
7  struct Stu{
8   int Id;
9   char Name[32];
10  float Mark;
11  };
12
13  void *fun1(void *arg)
14  {
15  struct Stu *tmp = (struct Stu *)arg;
16  printf("%s:Id = %d Name = %s Mark = %.2f\n",__FUNCTION__,tmp->Id,tmp->Name,tmp->Mark);
17  
18  }
19
20  int main()
21  {
22
23  pthread_t tid1,tid2;
24  struct Stu stu;
25  stu.Id = 10000;
26  strcpy(stu.Name,"ZhangSan");
27  stu.Mark = 94.6;
28
29  int ret = pthread_create(&tid1,NULL,fun1,(void *)&stu);
30  if(ret != 0){
31    perror("pthread_create");
32    return -1;
33  }
34  printf("%s:Id = %d Name = %s Mark = %.2f\n",__FUNCTION__,stu.Id,stu.Name,stu.Mark);
35  sleep(1);
36  return 0;
37  }
38


运行结果:

1670913243611.jpg


1.1.6线程的退出与回收


线程的退出情况有三种:第一种是进程结束,进程中所有的线程也会随之结束。第二种是通过函数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。


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

pthread_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一致。


程序示例1

使用GIT下载所有源码后,本节源码位于如下目录:

01_all_series_quickstart\
04_嵌入式Linux应用开发基础知识\source\13_thread\01_文档配套源码
Pthread_Text6.c


测试例程6:(Phtread_txex6.c)

1  #include <pthread.h>
2  #include <stdio.h>
3  #include <unistd.h>
4  #include <errno.h>
5 
6  void *fun1(void *arg)
7  {
8   static int tmp = 0;//必须要static修饰,否则pthread_join无法获取到正确值
9   //int tmp = 0;
10  tmp = *(int *)arg;
11  tmp+=100;
12  printf("%s:Addr = %p tmp = %d\n",__FUNCTION__,&tmp,tmp);
13  pthread_exit((void *)&tmp);//将变量tmp取地址转化为void*类型传出
14  }
15
16
17  int main()
18  {
19
20  pthread_t tid1;
21  int a = 50;
22  void *Tmp = NULL;//因pthread_join第二个参数为void**类型
23  int ret = pthread_create(&tid1,NULL,fun1,(void *)&a);
24  if(ret != 0){
25    perror("pthread_create");
26    return -1;
27  }
28  pthread_join(tid1,&Tmp);
29  printf("%s:Addr = %p Val = %d\n",__FUNCTION__,Tmp,*(int *)Tmp);
30  return 0;
31  }
32


运行结果:

1670913382916.jpg

1670913390299.jpg


上述例程先通过23行将变量以地址的形式传入线程,在线程中做出了自加100的操作,当线程退出的时候通过线程传参,用void*类型的数据通过pthread_join接受。此例程去掉了之前加入的sleep函数,原因是pthread_join函数具备阻塞的特性,直至成功收回掉线程后才会冲破阻塞,因此不需要靠考虑主线程会执行到30行结束进程的情况。特别要说明的是例程第8行,当变量从线程传出的时候,需要加static修饰,对生命周期做出延续,否则无法传出正确的变量值。


程序示例2

使用GIT下载所有源码后,本节源码位于如下目录:

01_all_series_quickstart\
04_嵌入式Linux应用开发基础知识\source\13_thread\01_文档配套源码


Pthread_Text7.c

测试例程7:(Phtread_txex7.c)

1  #define _GNU_SOURCE 
2  #include <pthread.h>
3  #include <stdio.h>
4  #include <unistd.h>
5  #include <errno.h>
6 
7  void *fun(void *arg)
8  {
9   printf("Pthread:%d Come !\n",(int )(long)arg+1);
10  pthread_exit(arg);
11  }
12
13
14  int main()
15  {
16  int ret,i,flag = 0;
17  void *Tmp = NULL;
18  pthread_t tid[3];
19  for(i = 0;i < 3;i++){
20    ret = pthread_create(&tid[i],NULL,fun,(void *)(long)i);
21    if(ret != 0){
22    perror("pthread_create");
23    return -1;
24    }
25  }
26  while(1){//通过非阻塞方式收回线程,每次成功回收一个线程变量自增,直至3个线程全数回收
27    for(i = 0;i <3;i++){
28    if(pthread_tryjoin_np(tid[i],&Tmp) == 0){
29      printf("Pthread : %d exit !\n",(int )(long )Tmp+1);
30      flag++; 
31    }
32    }
33    if(flag >= 3) break;
34  }
35  return 0;
36  }
37


运行结果:

1670913418731.jpg

例程7展示了如何使用非阻塞方式来回收线程,此外也展示了多个线程可以指向同一个回调函数的情况。例程6通过阻塞方式回收线程几乎规定了线程回收的顺序,若最先回收的线程未退出,则一直会被阻塞,导致后续先退出的线程无法及时的回收。

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

7. 程序示例3

使用GIT下载所有源码后,本节源码位于如下目录:

01_all_series_quickstart\
04_嵌入式Linux应用开发基础知识\source\13_thread\01_文档配套源码
Pthread_Text8.c


测试例程8:(Phtread_txex8.c)

1  #define _GNU_SOURCE 
2  #include <pthread.h>
3  #include <stdio.h>
4  #include <unistd.h>
5  #include <errno.h>
6 
7  void *fun1(void *arg)
8  {
9   printf("Pthread:1 come!\n");
10  while(1){
11    sleep(1);
12  }
13  }
14
15  void *fun2(void *arg)
16  {
17  printf("Pthread:2 come!\n");
18  pthread_cancel((pthread_t )(long)arg);//杀死线程1,使之强制退出
19  pthread_exit(NULL);
20  }
21
22  int main()
23  {
24  int ret,i,flag = 0;
25  void *Tmp = NULL;
26  pthread_t tid[2];
27  ret = pthread_create(&tid[0],NULL,fun1,NULL);
28  if(ret != 0){
29    perror("pthread_create");
30    return -1;
31  }
32  sleep(1);
33  ret = pthread_create(&tid[1],NULL,fun2,(void *)tid[0]);//传输线程1的线程号
34  if(ret != 0){
35    perror("pthread_create");
36    return -1;
37  }
38  while(1){//通过非阻塞方式收回线程,每次成功回收一个线程变量自增,直至2个线程全数回收
39    for(i = 0;i <2;i++){
40    if(pthread_tryjoin_np(tid[i],NULL) == 0){
41      printf("Pthread : %d exit !\n",i+1);
42      flag++; 
43    }
44    }
45    if(flag >= 2) break;
46  }
47  return 0;
48  }
49


运行结果:

1670913448242.jpg

例程8展示了如何利用pthread_cancel函数主动的将某个线程结束。27行与33行创建了线程,将第一个线程的线程号传参形式传入了第二个线程。第一个的线程执行死循环睡眠逻辑,理论上除非进程结束,其永远不会结束,但在第二个线程中调用了pthread_cancel函数,相当于向该线程发送一个退出的指令,导致线程被退出,最终资源被非阻塞回收掉。此例程要注意第32行的sleep函数,一定要确保线程1先执行,因线程是无序执行,故加入该睡眠函数控制顺序,在本章后续,会讲解通过加锁、信号量等手段来合理的控制线程的临界资源访问与线程执行顺序控制。

相关文章
|
2天前
|
Ubuntu Linux 开发者
Ubuntu20.04搭建嵌入式linux网络加载内核、设备树和根文件系统
使用上述U-Boot命令配置并启动嵌入式设备。如果配置正确,设备将通过TFTP加载内核和设备树,并通过NFS挂载根文件系统。
29 15
|
16天前
|
存储 监控 Linux
嵌入式Linux系统编程 — 5.3 times、clock函数获取进程时间
在嵌入式Linux系统编程中,`times`和 `clock`函数是获取进程时间的两个重要工具。`times`函数提供了更详细的进程和子进程时间信息,而 `clock`函数则提供了更简单的处理器时间获取方法。根据具体需求选择合适的函数,可以更有效地进行性能分析和资源管理。通过本文的介绍,希望能帮助您更好地理解和使用这两个函数,提高嵌入式系统编程的效率和效果。
79 13
|
11天前
|
存储 安全 Java
Java多线程编程秘籍:各种方案一网打尽,不要错过!
Java 中实现多线程的方式主要有四种:继承 Thread 类、实现 Runnable 接口、实现 Callable 接口和使用线程池。每种方式各有优缺点,适用于不同的场景。继承 Thread 类最简单,实现 Runnable 接口更灵活,Callable 接口支持返回结果,线程池则便于管理和复用线程。实际应用中可根据需求选择合适的方式。此外,还介绍了多线程相关的常见面试问题及答案,涵盖线程概念、线程安全、线程池等知识点。
92 2
|
1月前
|
缓存 Java 调度
多线程编程核心:上下文切换深度解析
在现代计算机系统中,多线程编程已成为提高程序性能和响应速度的关键技术。然而,多线程编程中一个不可避免的概念就是上下文切换(Context Switching)。本文将深入探讨上下文切换的概念、原因、影响以及优化策略,帮助你在工作和学习中深入理解这一技术干货。
44 10
|
27天前
|
安全 算法 Java
Java多线程编程中的陷阱与最佳实践####
本文探讨了Java多线程编程中常见的陷阱,并介绍了如何通过最佳实践来避免这些问题。我们将从基础概念入手,逐步深入到具体的代码示例,帮助开发者更好地理解和应用多线程技术。无论是初学者还是有经验的开发者,都能从中获得有价值的见解和建议。 ####
|
27天前
|
Java 调度
Java中的多线程编程与并发控制
本文深入探讨了Java编程语言中多线程编程的基础知识和并发控制机制。文章首先介绍了多线程的基本概念,包括线程的定义、生命周期以及在Java中创建和管理线程的方法。接着,详细讲解了Java提供的同步机制,如synchronized关键字、wait()和notify()方法等,以及如何通过这些机制实现线程间的协调与通信。最后,本文还讨论了一些常见的并发问题,例如死锁、竞态条件等,并提供了相应的解决策略。
50 3
|
1月前
|
算法 调度 开发者
多线程编程核心:上下文切换深度解析
在多线程编程中,上下文切换是一个至关重要的概念,它直接影响到程序的性能和响应速度。本文将深入探讨上下文切换的含义、原因、影响以及如何优化,帮助你在工作和学习中更好地理解和应用多线程技术。
40 4
|
8天前
|
安全 Java API
【JavaEE】多线程编程引入——认识Thread类
Thread类,Thread中的run方法,在编程中怎么调度多线程
|
1天前
|
存储 人工智能 JavaScript
Harmony OS开发-ArkTS语言速成二
本文介绍了ArkTS基础语法,包括三种基本数据类型(string、number、boolean)和变量的使用。重点讲解了let、const和var的区别,涵盖作用域、变量提升、重新赋值及初始化等方面。期待与你共同进步!
61 47
Harmony OS开发-ArkTS语言速成二
|
4天前
|
API 索引
鸿蒙开发:实现一个超简单的网格拖拽
实现拖拽,最重要的三个方法就是,打开编辑状态editMode,实现onItemDragStart和onItemDrop,设置拖拽移动动画和交换数据,如果想到开启补位动画,还需要实现supportAnimation方法。
56 13
鸿蒙开发:实现一个超简单的网格拖拽