Linux多线程

Linux多线程

文章目录

1.Linux线程概念

1.1 什么是线程

  • 站在内核角度来理解进程:承担分配系统资源的基本实体,叫做进程
  • 在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列"
  • 一切进程至少都有一个执行线程
  • 线程在进程内部运行,本质是在进程地址空间内运行
  • 在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化
  • 透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流

需要明确的是,一个进程的创建实际上伴随着其进程控制块(task_struct)、进程地址空间(mm_struct)以及页表的创建,虚拟地址和物理地址就是通过页表建立映射的,如下图:

请添加图片描述

每个进程都有自己独立的进程地址空间和独立的页表,也就意味着所有进程在运行时本身就具有独立性,但如果我们在创建“进程”时,只创建task_struct,并要求创建出来的task_struct和父task_struct共享进程地址空间和页表,那么创建的结果就是下面这样的:

请添加图片描述

此时我们创建的实际上就是四个线程:

  • 其中每一个线程都是当前进程里面的一个执行流,也就是我们常说的“线程是进程内部的一个执行分支
  • 同时我们也可以看出,线程在进程内部运行,本质就是线程在进程地址空间内运行,也就是说曾经这个进程申请的所有资源,几乎都是被所有线程共享的

线程与进程的包含问题:

  • 下面用蓝色方框框起来的内容,我们将这个整体叫做进程,进程包含线程

请添加图片描述

  • 因此,所谓的进程并不是通过task_struct来衡量的,除了task_struct之外,一个进程还要有进程地址空间、文件、信号等等,合起来称之为一个进程

1.2 线程的执行流

  • 线程是进程的一个执行分支,是在进程内部运行的一个执行流,是操作系统进行运算调度的最小单位
  • 在linux里我们也把线程成为轻量级进程(LWP,LightWeightProcess),因为linux里其实没有真正的线程,线程是通过进程模拟出来的(在内核里都是一个个的task_struct)
  • 没学线程前我们说进程是操作系统最小的调度单位,因为那时我们写的代码都是单线程的,一个进程只有一个执行流,所以那么说也没错,准确一点就是线程是操作系统调度的最小单位

线程的执行流分为两种:单执行流与多执行流

请添加图片描述

请添加图片描述

【重要】通过上面两张图得出的一些结论,同时补充一些概念:

  • 站在CPU的角度,进程与线程没有任何区别,它看到的都是task_struct,这也是为什么线程在linux里也叫轻量级进程,也说明linux下没有真正意义上的线程。简单来说,CPU对线程0感知
  • 线程之间是共用一个地址空间的,这说明比起进程之间的切换,线程的切换更加轻量级。因为进程切换可能要保存页表、地址空间的数据甚至缓存的数据等等,而线程之间切换时这些都不用动,自然切换的代价也比较小
  • 进程:线程=1:n,说明系统内有大量的线程,所以操作系统必定要把线程管理起来,说明如果一个系统支持真正的线程,比如windows,那必然是有一个结构(TCB)来描述这个线程的属性,并且将其组织起来,但是往往比较复杂,linux下虽然没有真正的线程,但是优点就是简单
  • linux下没有真正意义的线程,这就说明OS不可能在系统层面提供操作线程的接口,而是一些封装好给用户的轻量级接口
  • CPU调度的都是线程,即线程是CPU调度的最小单位
  • 站在系统的角度,进程是承担系统资源的基本单位。因为第二个线程用的资源都是第一个线程申请好的
  • 线程在进程内部运行,这句话的意思是线程在进程地址空间内运行
  • 通过页表可以看到物理内存,也即真正的资源,同时说明只要划分页表我们就可以让线程看到进程的部分资源
  • 关于页表,页表不仅仅记录了虚拟地址与物理地址的映射,还有一些别的属性,如权限,是否命中等等,这说明页表的大小不是一个字节就能记录的。一般32位的机器物理内存是4G,说明有232这么多个地址要映射,即页表要记录232个映射关系,表示每一个映射关系需要的字节都大于1,说明页表整体大小大于4G,放不进内存,此时就引出了二级页表。负责这块的硬件就是MMU,具体的了解可以查询二级页表的相关资料
  • 线程数越多越好吗?并不是,建议与计算机的核数相当。线程过多会导致大部分时间花在调度上,而没有花在线程的执行上,有点买椟还珠的意思
  • 没有线程替换,线程替换就是整个进程被替换
  • CPU不需要线程的概念,linux下线程这个概念是给用户的,因为用户需要多线程编程
  • 用户层通过TCB(线程控制块)来知道线程的id、状态、优先级和其他属性,用来进行用户级的线程管理。TCB不由内核维护,而是由用户空间维护

1.3 线程的优缺点

线程的优点:

  • 创建一个新的线程的代价比创建一个进程小得多。创建一个线程虽然也需要创建数据结构,但是并不需要重新开辟资源,只需要将进程的部分资源分配给线程。创建一个进程不仅需要创建大量数据结构,还需要重新创建资源
  • 与进程之间的切换相比,线程之间的切换需要操作系统做的工作少。线程只是进程的部分资源,切换的资源少
  • 线程占用的资源比进程少
  • 能充分利用多处理器的可并行数量
  • 在等待慢速的I/O操作结束的同时,程序可以执行其它的计算任务
  • 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
  • I/O密集型应用,为了提高性能,将I/O操作重叠,线程可以同时等待不同的I/O操作。I/O操作是与外设交互数据,会很慢

线程的缺点:

  1. 性能缺失
    • 一个处理器只能处理一个线程,如果线程数比可用处理器数多,会有较大的性能损失,会增加额外的同步和调度开销,而资源不变
  2. 鲁棒性降低
    • 编写多线程时,可能因为共享了不该共享的变量,一个线程修改了该变量会影响另外一个线程。多线程之间变量时同一个变量,多进程之间变量不是同一个变量,写时拷贝
  3. 缺乏访问的控制
    • 进程时访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响
  4. 编程难度高

1.4 线程的异常与用途

线程的异常:

  • 单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃
  • 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出

线程的用途:

  • 合理的使用多线程,能提高CPU密集型程序的执行效率
  • 合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)

2.进程与线程的区别总结

  1. 进程是资源分配的基本单位,线程是调度的基本单位
  2. 进程:线程 = 1:n
  3. Linux中没有真正意义上的线程,线程是用进程模拟的
  4. 线程又称为轻量级进程
  5. 线程共享进程数据,但也拥有自己的一部分数据
  6. 进程的健壮性比线程好
  7. 多线程比进程消耗资源少,切换快,运行效率高

请添加图片描述


3.多进程与多线程的应用场景

多进程应用场景:shell、守护进程、分布式服务

请添加图片描述

请添加图片描述

多线应用场景:

  • 不同任务间需要大量共享数据或频繁通信时
  • 提供非均质的服务(有优先级任务处理)事件响应有优先级
  • 单任务并行计算,在非CPU Bound的场景下提高响应速度,降低时延
  • 与人有IO交互的应用,良好的用户体验(键盘鼠标的输入,立刻响应)

4.线程控制

4.1 进程ID与线程ID

  • 在Linux中由于没有真正的线程,目前的线程都是用原生线程库(Nagtive POSIX Thread Library)来实现。在这种实现下,线程又被称作轻量级进程,因为线程仍然使用进程描述符task_struct,但是只是执行进程的部分内容

  • 没有线程之前,一个进程对应内核的一个进程描述符,对应进程的ID。引入线程之后,一个进程对应了一个或者多个线程,每一个线程作为CPU调度的基本单位,在内核态也有自己的ID

  • 线程组,多线程的进程,又被称为线程组。每一个线程在内核中都存在一个进程描述符(task_struct),因为Linux下,用进程来模拟线程。进程结构体中的pid,表明上看是进程ID,其实不是,它实际对应线程ID,进程描述符中的tgid,对应用户层面的进程ID

  • 我们来看看内核源码是什么样的:

请添加图片描述

  • 总结:进程有自己的ID在源码中是pid,线程也有自己的ID,在源码中是tgid

进程ID有什么用呢?可以表示线程属于哪个进程的。就可以知道进程有多少线程

在创建线程使用的函数pthread_create的第一个参数返回的也是线程的id但是和这里的线程id,不过,这里的线程id是用来标识线程的,后面有介绍创建线程函数返回的id

查看线程id的命令:ps -aL

请添加图片描述

  • PID显示的是进程ID
  • LWP显示的是线程ID
  • 我们发现进程mythread有两个线程,一个线程的id是7854,一个线程的ID是7855。整个进程的ID是7854
  • 但是有一个线程的ID和进程的ID相同,这不是巧合。线程组(进程)里的第一个线程,在用户态被称为主线程,在内核中被称为group leader。线程中创建的第一个线程,会将该线程的ID设置成和线程组的ID相同。所以线程组内存在一个线程ID和进程ID相同,这个线程为线程组的主线程
  • 至于线程组的其它线程ID则由内核负责分配。线程组的ID总和主线程ID一致
  • 一个进程至少有一个线程。如果没有创建线程,该进程就是单线程的单进程

请添加图片描述

  • 注意:线程和进程不一样,进程有父子进程的概念,但是线程没有,所有进程都是对等的关系

4.2 线程创建(pthread_create)

创建线程库函数是:pthread_create

请添加图片描述

这个函数明确说明了需要链接库名-pthread,我们需要思考一些问题:

为什么连接线程库要指明库名?标准库不用指明库名?

  • 因为标准库是语言自带的,第三方库不是语言自带的,可能是系统或者是用户自己安装的,线程库是Linux系统安装的,不是语言提供的,对于gcc编译器来说是第三方库。gcc默认连接库是标准库(语言提供的)。编译器命令行参数中没有第三方库的名字。所以给编译器指明库名
  • 强调:找到库所在路径和使用该路径下的库文件,是两码事。找到路径找不到库,还需要指明库名。标准库中因为编译器命令行中有该库名

让我们来看看使用的例子:

mythread.cpp代码:

#include<iostream>
#include<pthread.h>
#include<unistd.h>
using namespace std;
     	
void* thread_run(void* arg)
{
   
     while(1)
     {
   
    	  cout<<"i am "<<(char*)arg<<endl;
    	  sleep(1);
     }
}

int main()
{
   
    pthread_t tid;
	int ret=pthread_create(&tid,NULL,thread_run,(void*)"thread 1");
    if(ret!=0)
    {
   
        return -1;
    }
    while(1)
    {
   
        cout<<"i am main thread"<<endl;
        sleep(2);
    }
	return 0;
}

makefile代码:

mythread:mythread.cpp
	g++ $^ -o $@ -lpthread
.PHONY:clean
clean:
	rm -f mythread

例子结果:

请添加图片描述


4.3 线程终止(pthread_exit、pthread_cancel)

  • 注意:主线程退出,整个进程就退出了
  • 要某个线程终止而不让进程终止,有三种方法:
    1. 从线程函数return,这种情况对主线程不适用,因为主线程退出,整个进程就退出了
    2. 线程可以调用pthread_exit终止
    3. 一个线程可以调用pthread_cancel终止同一进程里的线程

新线程也可以用pthread_cancel终止主线程:

请添加图片描述

pthread_exit函数:

请添加图片描述

请添加图片描述

注意:使用return和pthread_exit返回的指针所指向的内存单元必须是全局或者是malloc分配的,不能是在线程函数栈上分配的,因为线程退出时,函数栈帧被释放了

pthread_cancel函数:

请添加图片描述

请添加图片描述

注意:不能使用exit(),exit的作用是不论在哪里调用,终止进程

请添加图片描述


4.4 线程等待(pthread_join)

线程为什么需要等待?

  • 已经退出的线程,其空间没有被释放,仍然在进程的地址空间内
  • 创建的新线程不会复用刚才退出线程的地址空间

请添加图片描述

  1. 默认以阻塞方式等待
  2. 线程退出和进程退出一样,有三种状态:
    • 代码正常运行,结果正确,正常退出
    • 代码正常运行,结果不正确,不正常退出
    • 代码出现异常,异常退出
    • 前两种情况以退出码来表述退出情况,后面一种以退出信号来表示

但是线程等待函数的第2个参数返回的是执行函数的返回值,也就是退出码,没有表示线程异常退出的情况,这是为什么的?

  • 因为某个线程如果运行异常终止,整个进程都会终止。进程异常终止,就属于进程的等待处理的范畴了,不属于线程范畴。比如:一个线程函数有除0操作,硬件MMU发现异常,操作系统收到异常,向该进程发出信号,终止进程。信号处理的单位是进程

总的来说就是,等待线程只关心正常运行的退出情况,获取退出码。不关心异常退出情况,异常退出情况上升至进程处理范畴

请添加图片描述

那么这里我们怎么获取退出码呢?

调用pthread_join函数的线程默认以阻塞方式等待线程id为thread参数的线程终止,线程以不同的方式终止,得到的终止状态不同

  • 如果线程通过return终止,pthread_join函数的第二个参数retval直接指向return后面的返回值
  • 如果线程通过pthread_exit终止,pthread_join函数的第二个参数retval直接指向pthread_exit参数‘
  • 如果线程通过被其它线程调用pthread_cancel终止,pthread_join函数的第二个参数retval直接存放的是一个常数宏PTHREAD CANCELED,值是-1。#define PTHREAD CANCELED (void *)-1
  • 如果对不关心返回值,可以将ret_val设为NULL

三种情况的返回值如下图:

return:

请添加图片描述


pthread_exit:

请添加图片描述


pthread_cancel:

请添加图片描述


4.5 线程ID及进程地址空间布局

  • 上面讨论的线程ID(LWP)属于进程调度范畴。因为线程是轻量级进程,是操作系统调度的基本单位,所以会需要一个ID来标识给线程
  • 这里讨论的线程ID,是创建线程函数pthread_create的第一个参数。该内存是线程第三方库为线程在内存中开辟的一块空间。该线程ID返回该空间的起始地址。这个进程ID数据线程库的范畴,线程库的后序操作,就是根据该线程ID来操作的

为什么线程ID返回的是起始地址?

由于Linux没有真正意义上的线程,线程管理需要线程库来做,线程库管理线程也是要先描述再组织,描述如下图,组织成一个数组,再返回数组的起始地址

请添加图片描述

可以通过函数查询当前线程ID:

请添加图片描述

请添加图片描述


4.6 POSIX线程库与内核线程的关系

  • 由于在Linux中没有真正的线程,所以系统没有提供接口(系统调用),需要用户自己来编写。但是我们有一个第三方库,POSIX线程库给我们提供了管理线程的功能,但是线程需要内核来调度和执行
  • 要使用这些库函数需要引入头文件<pthread.h>
  • 链接这些线程函数库时要使用编译器命令"-lpthread"选项

请添加图片描述


4.3 pthread_create的传参问题

传参问题的探讨与验证:

  • 假设要往创建的工作线程中传入一个参数1,首先要将参数强转为(void*)类型,然后将参数的地址传入,而在工作线程中使用是只需将(void*)转换为(int*)即可,如下代码:
#include<iostream>
#include<unistd.h>
#include<pthread.h>
using namespace std;
     	
void* MyThreadStrat(void* arg)
{
   
     int* i=(int*)arg;//(void*)变成了(int*)
     while(1)
     {
   
    	  cout<<"MyThreadStrat:"<<*i<<endl;
    	  sleep(1);
     }
    return NULL;
}

int main()
{
   
    pthread_t tid;
    int i=1;
    int ret=pthread_create(&tid,NULL,MyThreadStrat,(void*)&i);
    if(ret!=0)
    {
   
    	 cout<<"线程创建失败!"<<endl;
    	 return 0;
   	}
    while(1)
	{
   
	     sleep(1);
	     cout<<"i am main thread"<<endl;
	 }
	 return 0;
}

请添加图片描述

从上面的结果可以看出,虽然参数可以正常传入,但实际是存在一定的错误的,因为局部变量 i 传入的时候生命周期未结束,而在传递给工作线程的时候生命周期结束了,那么这块局部变量开辟的区域就会自动释放,而此时工作线程还在访问这块地址,就会出现非法访问

让我们将代码改成循环的来看看:

#include<iostream>
#include<unistd.h>
#include<pthread.h>
using namespace std;
     	
void* MyThreadStrat(void* arg)
{
   
    int* i=(int*)arg;
    while(1)
    {
   
    	 cout<<"MyThreadStrat:"<<*i<<endl;
    	 sleep(1);
    }
    return NULL;
}

int main()
{
   
    pthread_t tid;
    int i=0;
    for( i=0;i<4;i++)
    {
   
    	int ret=pthread_create(&tid,NULL,MyThreadStrat,(void*)&i);
    	if(ret!=0)
    	
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

「已注销」

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值