【Linux】多线程概念线程控制

news/2025/1/13 7:54:31/

文章目录

  • 多线程概念
    • Linux下进程和线程的关系
    • pid本质上是轻量级进程id,换句话说,就是线程ID
    • Linux内核是如何创建一个线程的
    • 线程的共享和独有
    • 线程的优缺点
  • 线程控制
    • POSIX线程库
    • 线程创建
    • 线程终止
    • 线程等待
    • 线程分离

多线程概念

Linux下进程和线程的关系

在《程序员的自我修养》这本书中,对Linux下的多线程做了这样的描述:

Windows对进程和线程的实现如同教科书一样标准,Windows内核有明确的线程和进程的概念。在Windows API可以使用明确的API:CreateProcess和CreateThread来创建进程和线程,并且有一系列的API来操纵它们。但对于Linux来说,线程并不是一个通用的概念。
Linux对多线程的支持颇为贫乏,事实上,在Linux内核中并不存在真正意义上的线程的概念。
Linux将所有的执行实体(无论是线程还是进程)都称为任务(Task),每一个任务概念上都类似于一个单线程的进程,具有内存空间、执行实体、文件资源等。不过,Linux下不同的任务之间可以选择共享内存空间,因而在实际意义上,共享了同一个内存空间的多个任务构成了一个进程,这些任务也就成了这个进程里的线程

可以给出以下结论:

  • 线程是依附于进程才能存在的,如果没有进程,则线程不会单独存在
  • 多线程的存在是为了提高整个程序的运行效率的
  • 线程也被称为执行流,一个线程是执行代码的一个执行流,因为线程在执行用户写的代码,程序员创建的线程被称之为“工作线程”
  • Linux内核当中没有线程的概念,只有轻量级进程(LWP),线程是C库中的概念

pid本质上是轻量级进程id,换句话说,就是线程ID

  • 在task_struct结构体当中:
    • pid_t pid:轻量级进程id,也被称为线程id,不同的线程拥有不用的pid
    • pid_t tgid:轻量级进程组id,也被称为进程id,一个进程当中的线程拥有相同的tgid
  • 为什么在进程概念的介绍中,说pid就是进程 id?
    • 线程因为主线程的pid和tgid相等,而我们当时进程中只有一个主线程。所以我们的pid就等于tgid。所以将pid成为进程id也就是现在的tgid。

Linux内核是如何创建一个线程的

其本质就是再在当前进程组中创建一个task_struct结构体,它拥有着和主线程不同的pid,指向同一块虚拟进程地址空间。

在这里插入图片描述

线程的共享和独有

独有:

在进程虚拟地址空间的共享区当中,调用栈,寄存器, 线程ID,errno,信号屏蔽字, 调度优先级独有

  • 调用栈独享
    在这里插入图片描述

  • 寄存器独享:

    当操作系统调度进程的时候一定是以task_struct结构体调度,而task _struc结构体是以双向链表存储,而操作系统调度时是从就绪队列中调度已经就绪的进程,在这里也就是轻量级进程-线程,当调度时一定会有其他线程被切出,而它切出时寄存器中存储的就是当前要执行的指令,所以要用结构体中上下文信息保存

  • 线程ID独享:每个线程就是一个轻量级进程,所以它有自己的pid

  • errno独享:当线程在执行出错时会返回一个errno,这个errno属于当前自己的线程错误

  • 信号屏蔽字独享:阻塞位图

  • 调度优先级独享:每个进程/线程在执行时被调度的优先顺序

共享:

共享:文件描述符表,用户id,用户组id,信号处理方式,当前进程的工作目录

线程的优缺点

优先:

  • 多线程的程序,拥有多个执行流,合理使用(要保证结果运行结构正确,例如多个进程并发执行就可能会出现同时更改一块内存,从而出现运行结果错误,要控制线程的访问时序问题), 可以提高程序的运行效率
  • 多线程程序的线程切换比多进程程序快,付出的代价小 (因为这些线程指向同一个进程虚拟地址空间,有些可以共享的数据,比如:全局变量就能在线程切换的时候,不进行切换可以充分发挥多核CPU并行(并行就是有多个CPU,每个CPU执行一个线程,各自执行各自的)的优势
  • 计算密集型的程序,可以进行拆分,让不同的线程执行计算不一 样的事情(比如我们要从1加到1亿我们可以让多个进程来各自计算其中一段加法,可以更快的得出结果)
  • I/O密集型的程序,可以进行拆分, 让不同的线程执行不同的I/O操作,可以不用串型运行, 提高程序运行效率。比如:我们要从多个文件中读取内容,如果我们只有一个进程的话,那就只能从一个文件中读取之后,在从下一个文件中读取,这样的串行运行,但是当我们有多个进程,就可以让多个进程从多个文件中同时读取。但也不是所有问题都可以拆分成多个进程去分开解决,一个女人花十个月可以生出一个孩子,但是十个女人不能再一个月生出一个孩子(《程序员的自我修养》)
    再比如:scanf是一个阻塞函数,假如printf函数前面有scanf需要被执行,这样,在scanf没有完成的时候,就不能往下执行printf,但是我们让两个线程来分别来执行scanf和printf,这样,就不存在被scanf阻塞,而后面的程序无法执行的问题了
    在这里插入图片描述

缺点:

  • 编写代码的难度更加高(当多个线程访问同一个程序的时候我们需要控制线程访问的先后顺序,要不然就可能出现问题)
  • 代码的(稳定性)鲁棒性要求更加高,一个线程崩溃,会导致整个进程退出。当多个线程在运行时,而CPU资源少的情况下一定是有一线程访问不到CPU资源的,那这时就一定要有线程被切换出来,将CPU资源让出来,这时一旦有线程霸占CPU资源占着不放的话,此时这个得不到CPU资源的线程就有可能崩溃,一旦它崩溃就会导致整个进程退出
  • 线程数量并不是越多越好,线程的切换是需要时间的,所以一个程序的线程数量一定是我们依照一个机器的配置(CPU数量)而经过测量来得出,创建多少个线程合适

在这里插入图片描述

  • 缺乏访问控制,多个线程同时访问一个空间,如果不加以控制,可能会导致程序产生二义性结果

线程控制

POSIX线程库

  • 线程相关的函数构成的函数库,绝大多数函数是以pthread_开头的
  • 使用这些线程函数需要引入头文件pthread.h
  • 编译含有多线程函数的源文件时,要加上编译命令-lpthread选项

线程创建

函数:

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

参数:

  • thread:线程标识符,是一个输出型参数,本质上是线程独有空间的首地址
  • attr:线程的属性信息,一般不设置属性信息,传递NULL,采用默认的线程属性;如果要设置属性信息,一般我们关心下列属性信息:调用栈的大小、分离属性、调度策略、分时策略、调度优先级等等
  • start_routine:函数指针,线程执行的入口函数(线程执行起来的时候,从该函数开始运行,注意:不是从main函数开始运行),当前线程执行时都是从线程入口函数开始执行
  • arg:传递给线程入口函数的参数,也就是给start_routine传递参数
    返回值:
  • 成功返回0
  • 失败返回<0
    线程创建代码演示:
    1 #include<stdio.h>2 #include<unistd.h>3 #include<pthread.h>4 
W>  5 void* mythread_start(void* arg){6   printf("I am work thread\n");
W>  7 }8 9 int main(){10   pthread_t tid;11   int ret = pthread_create(&tid, NULL, mythread_start, NULL);                                12   if(ret < 0){13     perror("pthread_create");14     return 0;15   }16   return 0;17 }

注意在makefile文件中链接线程库

执行结果:

在这里插入图片描述

很遗憾我们没看到应该存在的输出,这是什么原因呢?

因为线程都是需要操作系统进行调度的,我们在main函数中创建出来一个线程,但是我们的线程还没被调度,main线程就执行结束返回了,main函数返回就意味着进程结束了,进程结束了我们的线程就不存在了,自然不会再给出任何打印。

那我们想看到现象要怎么做呢?很容易想到,让main函数晚一点退出,给工作线程充足的时间能被操作系统调度,我们让main函数在返回前执行sleep(2);

再执行:可以看到工作线程执行了它的代码

为了观察一下线程堆栈和函数堆栈,我们索性让main函数和线程入口函数都进入睡眠状态,修改后代码如下:

    1 #include<stdio.h>2 #include<unistd.h>3 #include<pthread.h>4 
W>  5 void* mythread_start(void* arg){6   while(1){7     sleep(1);8     printf("I am work thread\n");9   }10 }11 12 int main(){13   pthread_t tid;14   int ret = pthread_create(&tid, NULL, mythread_start, NULL);15   if(ret < 0){16     perror("pthread_create");17     return 0;18   }19   while(1){20     sleep(1);                                                                                21   }22   return 0;23 }

我们看看此时的调用堆栈:

在这里插入图片描述
可以用top -H -p [pid]查看进程状态信息;

我们试着给线程传递一下局部变量,代码如下:

    1 #include<stdio.h>2 #include<unistd.h>3 #include<pthread.h>4 5 void* mythread_start(void* arg){6   int* i = (int*)arg;7   printf("I am work thread %d\n", *i);
W>  8 }9 10 int main(){11   pthread_t tid;12   for(int i=0; i<5; ++i){13     int ret = pthread_create(&tid, NULL, mythread_start, (void*)&i);               14     if(ret < 0){15       perror("pthread_create");16       return 0;17     }18   }19   sleep(1);20   return 0;21 }

观察一下程序执行结果:

在这里插入图片描述

我们的预期是打印0-4的数字,但是执行几次发现,首先每次执行结果并不一样,其次并不按照我们预期的结果进行打印,这是怎么回事呢?是因为线程是抢占式执行的,可能是我们将所有的线程创建出来,再去执行线程的代码,或者说一边创建一边执行代码, 线程的执行顺序不确定,某个线程访问数据的时间也不确定,导致了我们上述那么多种执行结果,还有一种结果是访问数据5,i是我们for循环种的局部变量,如果for循环退出后还有线程去访问i,这是十分危险的,因为i已经被释放了,此时再对它进行访问,就有可能导致错误。

解决上面的方式有两种一种是在main函数中创建一个变量,只要main函数存在,其他那个变量就存在。而main函数退出线程也就退出了,不存在非法访问。这种是解决非法访问的问题。

另一种方式是在堆上申请空间,这样保证每个进程访问的数据是自己对应的数据

    1 #include<stdio.h>2 #include<unistd.h>3 #include<pthread.h>4 #include<stdlib.h>5 6 void* mythread_start(void* arg){7   int* p = (int*)arg;8   printf("I am work thread%d\n", *p);                                              9   free(p);
W> 10 }11 12 int main(){13   pthread_t tid;14   for(int i=0; i<5; i++){15     int *p = (int*)malloc(sizeof(int));16     *p = i;17     int ret = pthread_create(&tid, NULL, mythread_start, (void*)p);18     if(ret < 0){19       perror("pthread_create");20       return 0;21     }22   }23   sleep(1);24   return 0;25 }

执行结果:

在这里插入图片描述

总结:

  • 不要给线程传递临时变量,因为传递临时变量当临时变量销毁时,线程拿到的是临时变量的地址,还可以访问那块被释放的空间,容易造成进程崩溃
  • 线程入口函数传递参数的时候,传递堆区空间,释放堆区空间的时候,让线程自己释放

线程终止

线程终止的方法:

1、从线程入口函数种return返回
2、pthread_exit(void* retval)函数,retval:线程退出时, 传递给等待线程的退出信息;作用:
谁调用谁退出,主线程调用主线程终止,工作线程调用工作线程终止
3、pthread_cancel(pthread_t)参数是一个线程标识符,想要取消哪个线程,就传递哪个线程的标识符

补充一个函数:pthread_t pthread_self(void):返回调用此函数的线程id

代码演示:

  • 创建一个线程,然后在main函数中终止这个线程,为了防止是进程结束,而导致线程也结束我们在main函数中加一个死循环

代码如下:

    1 #include<stdio.h>2 #include<unistd.h>3 #include<pthread.h>4 
W>  5 void* pthread_start(void *arg){6   printf("I am work pthread\n");7   while(1){8     sleep(1);9     printf("I am work thread-l\n");10   }11 }12 int main(){13   pthread_t tid;14   int ret = pthread_create(&tid, NULL, pthread_start, NULL);15   if(ret < 0){16     perror("pthread_create");17     return 0;18   }19   pthread_cancel(tid);20   while(1){21     sleep(1);22     printf("I am main pthread\n");23   }24   return 0;25 } 

执行结果:

在这里插入图片描述

能看到线程并没有立即终止,而是执行了一下线程种的命令然后才终止

  • 观察结束主线程,而不结束工作线程,会出现什么现象

代码如下:

    1 #include<stdio.h>2 #include<unistd.h>3 #include<pthread.h>4 
W>  5 void* pthread_start(void *arg){6   printf("I am work pthread\n");7   while(1){                       8     sleep(1);                     9     printf("I am work thread-l\n");10   }                               11 }                                 12 int main(){                       13   pthread_t tid;                  14   int ret = pthread_create(&tid, NULL, pthread_start, NULL);15   if(ret < 0){                    16     perror("pthread_create");     17     return 0;                     18   }                               19   getchar();//设置阻塞,当接受到字符后主线程将结束                                      20   pthread_cancel(pthread_self());//结束主线程         21   while(1){                    22     sleep(1);                  23     printf("I am main pthread\n");24   }                            25   return 0;26 }

设置阻塞的目的是为了查看进程id,以观察进程

执行结果:

getchar之前的状态:

在这里插入图片描述
getchar之后的状态:

在这里插入图片描述
用ps aux | grep tex查看前后对比:

在这里插入图片描述

可以得出结论:主线程先退出,工作线程没退出,主线程变成僵尸进程

  • 验证pthread_cancle函数,结束一个线程时,它会执行下一行命令

代码思路:将while循环去掉,让线程退出的下一句代码是return 0,观察程序状况

代码如下:

    2 #include<unistd.h>3 #include<pthread.h>4 
W>  5 void* pthread_start(void *arg){6   printf("I am work pthread\n");7   while(1){8     sleep(1);9     printf("I am work thread-l\n");10   }11 }12 int main(){13   pthread_t tid;14   int ret = pthread_create(&tid, NULL, pthread_start, NULL);15   if(ret < 0){16     perror("pthread_create");17     return 0;18   }19   getchar();//设置阻塞,当接受到字符后主线程将结束20   pthread_cancel(pthread_self());//结束主线程21   //while(1){22   //  sleep(1);23   //  printf("I am main pthread\n");24   //}                                                                                     25   return 0;26 }

执行结果:

在这里插入图片描述

可以发现这次进程直接退出了,主线程也不是僵尸状态了,这时因为当我们执行pthread_cancle函数时,结束一个线程时,他会执行下一行命令,这时我们将主线程退出了,它在退出前执行了return 0,就会使得整个进程结束,那么此时工作线程也就退出了

  • 观察主线程先退出变成僵尸进程后,工作线程执行完后主线程的状态

代码思路:让主线程退出,然后工作线程等待10s之后退出

代码如下:

    1 #include<stdio.h>2 #include<unistd.h>3 #include<pthread.h>4 
W>  5 void* pthread_start(void *arg){6   int count = 30;7   while(count--){8     sleep(1);9     printf("I am work thread-l\n");10   }
W> 11 }12 int main(){                                                                                      13   pthread_t tid;14   int ret = pthread_create(&tid, NULL, pthread_start, NULL);15   if(ret < 0){16     perror("pthread_create");17     return 0;18   }19   //getchar();//设置阻塞,当接受到字符后主线程将结束20   pthread_cancel(pthread_self());//结束主线程21   while(1){22     sleep(1);23     printf("I am main pthread\n");24   }25   return 0;26 }

执行结果分析:

在这里插入图片描述

当主线程退出而工作线程不退出时,我们是无法看到进程的调用栈信息的

在这里插入图片描述
总结:

  • 当我们执行pthread_cancle函数时,结束一个线程时,他会执行pthread_cancle函数下一行命令,然后再结束线程
  • 当主线程退出后,工作线程如果依然在执行,主线程就会处于僵尸状态,而当工作线程执行完毕之后退出,整个进程也随之结束

线程等待

线程在创建出来的时候,属性默认是joinable属性,意味着线程在退出的时候需要其他执行流(线程)来回收线程的资源(主要是退出线程使用到的共享区当中的空间)

接口:

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

功能:若线程A调用了该函数等待B线程,A线程会阻塞,直到B线程退出后,A线程才会解除阻塞状态
参数:

  • pthread_t : 线程标识符,要等待哪一个线程,就传递哪个线程的标识符
  • retval : 保存的是一个常数,退出线程的退出信息
线程退出方式*retval保存的东西
return入口函数返回值
pthread_exit函数pthread_exit函数参数
pthread_cancel函数PTHREAD_CANCEL宏定义

返回值:成功返回0,失败返回错误码

代码思路:让工作线程等待30s退出,然后在主线程中等待工作线程退出

代码如下:

    1 #include<stdio.h>2 #include<unistd.h>3 #include<pthread.h>4 5 void* pthread_start(void *arg){6   int count = 30;7   while(count--){8     sleep(1);9     printf("I am work thread-l\n");10   }11 }12 int main(){13   pthread_t tid;14   int ret = pthread_create(&tid, NULL, pthread_start, NULL);15   if(ret < 0){16     perror("pthread_create");17     return 0;18   }19   pthread_join(tid, NULL);                                                                       20   return 0;21 }

执行分析:

在这里插入图片描述

线程分离

分离线程是将线程标记成已分离,其属性从joinable变成detach,对于detach属性的线程终止后,系统会自动回收其资源,不用任何线程回收其资源

接口:

int pthread_detach(pthread_t thread);

功能:将线程标记为已分离,目的是当分离的线程终止时,其资源会自动释放,防止产生僵尸进程,防止内存泄漏

参数pthread_t:需要标记分离的线程标识符

调用pthread_detach函数的位置可以是:

  • 在主线程中调用分离创建出来的线程,即主线程标记分离工作线程;
  • 在工作线程的线程入口函数中调用,即自己标记分离自己;
    线程分离的实质就是将线程的属性设置为detach
  • 工作线程退出,然后不回收它的退出状态信息

代码如下:

    1 #include<stdio.h>2 #include<unistd.h>3 #include<pthread.h>4 
W>  5 void* pthread_start(void *arg){6   int count = 10;7   while(count--){8     sleep(1);9     printf("I am work thread-l\n");10   }
W> 11 }12 int main(){13   pthread_t tid;14   int ret = pthread_create(&tid, NULL, pthread_start, NULL);15   if(ret < 0){16     perror("pthread_create");17     return 0;18   }19   //pthread_join(tid, NULL);20   while(1){21     sleep(1);22   }23   return 0;24 }

执行结果分析:

在这里插入图片描述

可以看到它运行完直接退出了,也没有变成僵尸状态

  • 将工作线程设置为分离状态,观察

代码如下:

    1 #include<stdio.h>2 #include<unistd.h>3 #include<pthread.h>4 
W>  5 void* pthread_start(void *arg){6   pthread_detach(pthread_self());                                                                7   int count = 30;8   while(count--){9     sleep(1);10     printf("I am work thread-l\n");11   }
W> 12 }13 int main(){14   pthread_t tid;15   int ret = pthread_create(&tid, NULL, pthread_start, NULL);16   if(ret < 0){17     perror("pthread_create");18     return 0;19   }20   //pthread_join(tid, NULL);21   while(1){22     sleep(1);23   }24   return 0;25 }

执行分析:

在这里插入图片描述
在这里插入图片描述

结论:无论其他线程等待不等待工作线程退出,回收它的退出状态信息,工作线程都不会变为僵尸状态。


http://www.ppmy.cn/news/1062866.html

相关文章

opencv/C++ 人脸检测

前言 本文使用的测试资源说明&#xff1a; opencv版本&#xff1a;opencv 4.6.0 人脸检测算法 Haar特征分类器 Haar特征分类器是一个XML文件&#xff0c;描述了人体各个部位的Haar特征值。包括&#xff1a;人脸、眼睛、鼻子、嘴等。 opencv 4.6.0自带的Haar特征分类器&…

代码随想录26|回溯总结

回溯总结:链接地址 回溯三部曲&#xff1a;参数、终止条件、for遍历(递归、回溯) 模板如下&#xff1a; void backtracking(参数) {if (终止条件) {存放结果;return;}for (选择&#xff1a;本层集合中元素&#xff08;树中节点孩子的数量就是集合的大小&#xff09;) {处理节点…

es和数据库同步方案

5.5 课程信息索引同步 5.5.1 技术方案 通过向索引中添加课程信息最终实现了课程的搜索&#xff0c;我们发现课程信息是先保存在关系数据库中&#xff0c;而后再写入索引&#xff0c;这个过程是将关系数据中的数据同步到elasticsearch索引中的过程&#xff0c;可以简单成为索引…

MIA文献阅读 ——医学图像处理在慢性肾脏疾病评估中的最新进展【2021】

目录 0 摘要1 引言2 磁共振成像(MRI)2.1 扩散磁共振成像2.1.1 扩散加权成像(DWI)2.1.2 扩散张量成像(DTI) 2.2 血氧水平依赖成像2.3 动脉自旋标记2.4 动态对比增强磁共振成像2.5 T1和T2映射2.6 磁化转移磁共振成像2.7 磁共振弹性成像2.8 其他磁共振成像技术 3 其他成像方式3.1 …

windows安装多个版本node

1.下载nvm-setup版本 安装过程会检测到你当前使用的node版本 提示 Node v12.14.0 is already installed. Do you want NVM to control this version? 翻译&#xff1a;已安装节点v12.14.0。你想让NVM控制这个版本吗? 选择 是(Y)。 nvm默认为我们增添了环境变量&#xff0c;…

STM32F4X Systick系统滴答定时器

STM32F4X Systick系统滴答定时器 Systick定时器Systick使用Systick时钟源Systick寄存器Systick频率计算Systick例程 Systick定时器 在以Crotex-M4为架构的MCU中&#xff0c;都会有一个Systick内核定时器&#xff0c;这个定时器的作用可以给系统一个心跳时钟&#xff0c;通常用…

通过运行中的容器生成 Docker Compose 配置文件

背景 笔者之前有一次不小心删除了原始的 docker-compose.yml 文件&#xff0c;不过正在运行的 Docker 容器还在&#xff0c;找了许久&#xff0c;发现一个方法可以从这些容器中生成一个等效的 Docker Compose 配置文件。本文将介绍使用 autocompose 工具从正在运行的容器中反向…

Linux Shell 如何列举当前目录下的文件夹

如果只是想列举每个文件夹 那么就使用 ls -d */ 命令&#xff0c;因为 文件夹的名称都是以 / 结尾的 如果想要列出每个文件夹还有文件夹里包含什么文件等 那么就使用 ls */ 命令