Linux多线程---线程概念和线程控制

线程概念

什么是线程?

CPU视角:

线程是CPU调度的基本单位

与进程的关系:

线程是进程内部的执行流,线程比进程粒度更细,调度成本更低,切换成本更低

线程在进程内部运行,本质是在进程地址空间内运行,一个进程至少有一个执行线程

透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流

Linux下线程概念

在Linux系统的CPU眼中,看到的PCB其实就是轻量化进程,看图理解:
在这里插入图片描述

​ 将上图的所有的task_struct,进程地址空间,页表这三者之和称为进程,而对于每个task_struct就是线程。所以创建线程时只创建PCB,线程只占用进程地址空间中的一部分代码和一部分数据。

站在OS的角度重新理解进程:进程是承担分配系统资源的实体 (向系统申请资源的基本单位) 。

对于只有一个task_struct的进程就是单执行流进程,不止一个task_struct的进程就是多执行流进程。

那么CPU能分辨task_struct是进程和线程吗?

​ 答案:不能也不需要,在Linux中进程和线程没有概念上的区分,都叫执行流,CPU只需要关心一个个独立执行流。无论进程内部只有一个执行流还是有多个执行流,CPU都是以task_struct为单位进行调度的。

​ 类比理解:进程和线程关系就好像家庭和家庭成员,家庭是分配社会资源的基本载体,家庭成员占一部分资源。

​ 站在CPU的视角:task_struct <= 传统的进程(当且仅当进程是单执行流,等号成立),也就是所CPU看到的PCB比传统的进程更轻量化了 。

理解页表

为何以多级页表实现?

​ 以32位平台为例,会产生232个地址,倘若是一张页表建立虚拟地址和物理内存的映射,则需要232个页表项,页表项中还包含权限相关的信息,比如字符串常量不能修改,因为页表中有读写权限标识了变量的读写权限,这样一张页表的占据空间是巨大的,所以Linux下的页表是以多级页表实现的。

多级页表是如何实现的?

以32位平台为例,多级页表的组成:页目录 + 页表,将32位地址分成3部分=10+10+12

页目录:虚拟地址前10位 映射到-> 页表

页表:虚拟地址的中间10位 映射到-> page的起始地址(页框4KB)

画图理解:
在这里插入图片描述

物理内存被分成了一个个4KB大小的页框page,可见对内存的管理就是对数组struct page mem[1024*1024]的管理。

多级页表的优点

1.将进程虚拟地址管理和内存管理,通过页表+页框page解耦

2.使用分页机制和按需创建页表,节省了内存空间

多线程的特点

优点

线程创建:创建线程的代价比创建进程的代价小很多

线程切换:与进程切换相比,线程需要OS做的工作少很多

线程占有资源:线程占用的资源比进程少很多

利用多处理器:能充分利用多处理器的可并行数量

并行处理任务:在等待慢速IO的时候,程序可以执行其他计算任务

计算密集型应用:在多处理器系统上运行,不同CPU执行不同的计算线程

I/O密集型应用:将IO操作重叠,不同线程等待不同的IO操作,提高性能

缺点

性能损失:计算密集型应用的计算线程数量大于处理器数量,较大的性能消耗,增加额外的同步和调度开销,而可用的资源不变。

健壮性降低:编写多线程程序需要更全面深入的考虑,因为时间的影响导致不该共享的变量共享了会造成影响,因为多个线程使用同一个地址空间,会相互影响,又称线程之间是缺乏保护的。

缺乏访问控制:进程是访问控制的基本粒度,线程的IO函数会影响整个进程,比如不同同时往显示器打印

编程难度提高:排查问题难度更大,编写多线程程序比单线程程序更难

线程异常

​ 当一个线程异常的时候,会导致整个进程退出,因为线程异常的时候,就是进程异常了,进程就会收到OS发来的信号,进而进程退出。当创建新的线程异常的时候,新线程异常退出的信号是整个进程的,主线程不用主动获取。线程异常会使异常的线程影响其他的线程,所以线程的健壮性较低。

线程用途

1.提高CPU密集型程序的执行效率

2.提高IO密集型程序的用户体验

进程和线程的关系

​ 进程是OS分配资源的基本单位,线程是CPU调度的基本单位,因为线程共用一份地址空间,因此代码段(Text Segment、Data Segment),数据段都是共享的,定义函数、全局变量,在各线程都可以看到,除此之外,线程还共享:

文件描述符表

每种信号处理方式(SIG_IGN、SIG_DFL或者自定义的信号处理函数)

当前工作目录

用户 id 和 组 id

线程共享进程数据,也拥有自己的一部分数据:

线程 ID

一组寄存器

线程栈

errno(错误码)

信号屏蔽字

调度优先级

线程控制

​ 原生线程库:Linux下有一套遵从POSIX标准的线程库,与线程有关的函数构成了一个完整的序列,绝大多数函数名以pthread_开头,使用函数时需要引入头文件<pthread.h>,编译的时候需要使用-lpthread选项表示要链接线程库pthread。在C++11中也更新了线程库,C++的线程库也是封装了这套原生线程库的。

创建线程

使用函数pthread_create创建线程,函数原型:

在这里插入图片描述

参数说明:

thread:输出型参数,表示线程id

attr:设置创建线程的属性,传入NULL表示使用默认属性

start_routine:线程执行的函数,该函数的返回值类型是void*,作为线程等待的输出型参数

arg:线程执行的函数传入的参数

函数返回值:创建成功返回0,创建失败返回错误码
在这里插入图片描述

等待线程

使用函数pthread_join等待退出的线程,倘若不做此工作也没有分离线程,就会造成类似僵尸进程般的内存泄露。

函数原型:

在这里插入图片描述

参数说明:

thread:等待的线程id

retval:输出型参数,输出进程的退出码

函数返回值:等待成功返回0,错误时返回错误码

在这里插入图片描述

线程退出

退出方法:

1.在线程运行函数中return

void*threadRoutine(void*args)
{
	...
	return nullptr;
}

2.线程调用函数pthread_exit主动退出

在这里插入图片描述

3.线程调用pthread_cancle取消其他线程(通常是主线程调用)
在这里插入图片描述

注意:线程调用exit函数会退出进程,任何一个线程调用都代表整个进程退出。

线程分离

使用pthread_detach函数分离指定线程,相当于告诉OS线程退出时自动释放线程资源

在这里插入图片描述

可以让其他线程分离,或者自己主动分离。让主线程分离时,主线程退出相当于进程退出 ,分离主线程后,主线程一般不退出(常驻内存)。

线程分离相当于线程退出的第四种退出方式, 延后退出。

线程id

线程调用函数pthread_self可以获取线程的线程id

在这里插入图片描述

代码实例:

#include <pthread.h>
#include <iostream>
#include <unistd.h>
using namespace std;

void *threadRoutine(void *args)
{
    pthread_detach(pthread_self());
    for (int i = 0; i < 20; i++)
    {
        cout << "[" << pthread_self() << "] pthread " << (int)i << "is running..." << endl;
        sleep(1);
    }
    return nullptr;
}
int main()
{
    pthread_t tp[3];
    for (int i = 0; i < 3; i++)
    {
        pthread_create(&tp[i], nullptr, threadRoutine, (void *)i);
    }
    while (1)
    {
        cout << "main thread is running!" << endl;
        sleep(1);
    }
    return 0;
}

编译运行程序,使用ps -aL查看线程,L表示查看轻量级进程(LWP)。

在这里插入图片描述

可以看到多个线程往显示器打印出现了混乱。上图的LWP,是OS标识轻量级进程的编号,即线程:LWP=1:1。

​ 线程是独立执行流,在运行过程中会产生临时数据,需要有自己独立的栈结构,所有的代码执行都在进程的地址空间中,线程的全部实现,并没有体现在OS内,而是OS提供执行流,具体的线程结构由库来管理,在进程地址空间加载的共享区中的线程库维护着线程结构,每个用户级线程的控制结构体的起始地址就是线程id,如图:

在这里插入图片描述

线程的局部存储指的是线程私有的全局变量,指定线程的代码中在全局变量前加上 __thread可以使变量变成私有的全局变量。

链接的线程库:

在这里插入图片描述

线程控制块中有私有栈,底层是通过调用clone函数实现的:

在这里插入图片描述

这个函数可以创建共享内存空间的进程,其实是Linux下的线程的原型。

在这里插入图片描述

另外使用syscall(SYS_gettid)间接调用gettid 可以获得LWP,如下:

#include <unistd.h>
#include <sys/syscall.h>
#include <sys/types.h>

int main(int argc, char *argv[])
{
    pid_t tid;

    tid = syscall(SYS_gettid);
    tid = syscall(SYS_tgkill, getpid(), tid);
}

另外使用syscall(SYS_gettid)间接调用gettid 可以获得LWP,如下:

#include <unistd.h>
#include <sys/syscall.h>
#include <sys/types.h>

int main(int argc, char *argv[])
{
    pid_t tid;

    tid = syscall(SYS_gettid);
    tid = syscall(SYS_tgkill, getpid(), tid);
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值