嵌入式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先执行,因线程是无序执行,故加入该睡眠函数控制顺序,在本章后续,会讲解通过加锁、信号量等手段来合理的控制线程的临界资源访问与线程执行顺序控制。

相关文章
|
1月前
|
Linux
Linux编程: 在业务线程中注册和处理Linux信号
本文详细介绍了如何在Linux中通过在业务线程中注册和处理信号。我们讨论了信号的基本概念,并通过完整的代码示例展示了在业务线程中注册和处理信号的方法。通过正确地使用信号处理机制,可以提高程序的健壮性和响应能力。希望本文能帮助您更好地理解和应用Linux信号处理,提高开发效率和代码质量。
51 17
|
1月前
|
Linux
Linux编程: 在业务线程中注册和处理Linux信号
通过本文,您可以了解如何在业务线程中注册和处理Linux信号。正确处理信号可以提高程序的健壮性和稳定性。希望这些内容能帮助您更好地理解和应用Linux信号处理机制。
61 26
|
3月前
|
存储 监控 Linux
嵌入式Linux系统编程 — 5.3 times、clock函数获取进程时间
在嵌入式Linux系统编程中,`times`和 `clock`函数是获取进程时间的两个重要工具。`times`函数提供了更详细的进程和子进程时间信息,而 `clock`函数则提供了更简单的处理器时间获取方法。根据具体需求选择合适的函数,可以更有效地进行性能分析和资源管理。通过本文的介绍,希望能帮助您更好地理解和使用这两个函数,提高嵌入式系统编程的效率和效果。
140 13
|
3月前
|
存储 安全 Java
Java多线程编程秘籍:各种方案一网打尽,不要错过!
Java 中实现多线程的方式主要有四种:继承 Thread 类、实现 Runnable 接口、实现 Callable 接口和使用线程池。每种方式各有优缺点,适用于不同的场景。继承 Thread 类最简单,实现 Runnable 接口更灵活,Callable 接口支持返回结果,线程池则便于管理和复用线程。实际应用中可根据需求选择合适的方式。此外,还介绍了多线程相关的常见面试问题及答案,涵盖线程概念、线程安全、线程池等知识点。
291 2
|
3月前
|
安全 Java API
【JavaEE】多线程编程引入——认识Thread类
Thread类,Thread中的run方法,在编程中怎么调度多线程
|
5月前
|
安全 Linux
Linux线程(十一)线程互斥锁-条件变量详解
Linux线程(十一)线程互斥锁-条件变量详解
|
9月前
|
API
linux---线程互斥锁总结及代码实现
linux---线程互斥锁总结及代码实现
|
8月前
|
安全 算法 Linux
【Linux】线程安全——补充|互斥、锁|同步、条件变量(下)
【Linux】线程安全——补充|互斥、锁|同步、条件变量(下)
80 0
|
8月前
|
存储 安全 Linux
【Linux】线程安全——补充|互斥、锁|同步、条件变量(上)
【Linux】线程安全——补充|互斥、锁|同步、条件变量(上)
84 0
|
10月前
|
安全 Linux 调度
【linux线程(二)】线程互斥与线程同步
【linux线程(二)】线程互斥与线程同步

热门文章

最新文章

  • 1
    【01】噩梦终结flutter配安卓android鸿蒙harmonyOS 以及next调试环境配鸿蒙和ios真机调试环境-flutter项目安卓环境配置-gradle-agp-ndkVersion模拟器运行真机测试环境-本地环境搭建-如何快速搭建android本地运行环境-优雅草卓伊凡-很多人在这步就被难倒了
  • 2
    uniapp 极速上手鸿蒙开发
  • 3
    【04】鸿蒙实战应用开发-华为鸿蒙纯血操作系统Harmony OS NEXT-正确安装鸿蒙SDK-结构目录介绍-路由介绍-帧动画(ohos.animator)书写介绍-能够正常使用依赖库等-ArkUI基础组件介绍-全过程实战项目分享-从零开发到上线-优雅草卓伊凡
  • 4
    EMAS 性能分析全面适配HarmonyOS NEXT,开启原生应用性能优化新纪元
  • 5
    鸿蒙开发:了解@Builder装饰器
  • 6
    鸿蒙开发:wrapBuilder传递参数
  • 7
    鸿蒙web加载本地网页资源异常
  • 8
    【01】鸿蒙实战应用开发-华为鸿蒙纯血操作系统Harmony OS NEXT-项目开发实战-优雅草卓伊凡拟开发一个一站式家政服务平台-前期筹备-暂定取名斑马家政软件系统-本项目前端开源-服务端采用优雅草蜻蜓Z系统-搭配ruoyi框架admin后台-全过程实战项目分享-从零开发到上线
  • 9
    鸿蒙H5离线包技术分享
  • 10
    【02】鸿蒙实战应用开发-华为鸿蒙纯血操作系统Harmony OS NEXT-项目开发实战-准备工具安装-编译器DevEco Studio安装-arkts编程语言认识-编译器devco-鸿蒙SDK安装-模拟器环境调试-hyper虚拟化开启-全过程实战项目分享-从零开发到上线-优雅草卓伊凡