【Linux--多线程】

发布时间:2023年12月26日

前言

重谈地址空间–页表
在这里插入图片描述

一、线程的基本概念

1.1什么是线程

线程是进程内的一个执行分支,一个进程内有多行代码,线程通常情况下只执行这多行代码的部分代码。更准确的定义是:线程是“一个进程内部的控制序列”。
一个进程内至少有一个执行线程
线程在进程内部运行,本质是在进程地址空间内运行
在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化
透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流
在这里插入图片描述
操作系统中存在大量的进程,一个进程中又存在一个或多个线程,因此线程的数量一定比进程的数量多,很明显线程的执行粒度要比进程更细。

若一款操作系统要真正意义上支持线程,那么就需要对线程进行管理。比如创建线程、终止线程、调度线程、切换线程、给线程分配资源、释放资源以及回收资源等等,所有的这一套相比较进程都需要另起炉灶,搭建一套线程管理模块。

因此,若要支持真的线程一定会提高设计操作系统的复杂程度。在Linux看来,描述线程的控制块和描述进程的控制块是类似的,因此Linux并没有重新为线程设计管理模块,而是直接复用了进程控制块,即Linux中的所有执行流都是轻量级进程

但也有支持真正线程的操作系统,譬如Windows操作系统就存在专门描述线程的控制块,因此Windows操作系统系统的实现逻辑一定比Linux操作系统更为复杂

1.2线程的特点

1.2.1线程的优点

  1. 与进程切换相比,线程之间切换需要操作系统做的工作要少很多(cpu中有一个叫cache的寄存器,这个寄存器还是比较大的,里面缓存着热数据,这个热数据可以理解为高度频繁使用的数据,线程切换不需要重新加载热数据,而进程切换需要重新加载热数据)。
  2. 创建一个新线程的代价要比创建一个新进程小得多
  3. 线程占用的资源要比进程少很多
  4. 能充分利用多处理器的可并行数量
  5. 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
  6. I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。

概念说明:

计算密集型(CPU密集型):执行流的大部分任务,主要以计算为主。如加密解密、大数据查找等
IO密集型:执行流的大部分任务,主要以IO为主。如刷盘、访问数据库、访问网络等

1.2.2线程的缺点

  1. 编程难度高:编写与调式一个多线程程序比较困难
  2. 缺乏访问控制:在一个线程中调用某些OS函数会对整个进程造成影响。
  3. 健壮性降低:编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,即线程之间是缺乏保护的,此外一个线程崩溃会导致整个进程崩溃
  4. 性能损失:一个很少被外部事件阻塞的计算密集型线程往往无法与其他线程共享同一个处理器。若计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失(即增加了额外的同步和调度开销,而可用的资源不变)

1.2.3线程异常

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

1.2.4线程用途

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

1.3进程与线程的区别

线程共享进程数据,但是也拥有自己的一部分数据:
栈、线程ID、一组寄存器(用来恢复上下文)、errno、信号屏蔽字、调度优先级
进程的多个线程共享同一块地址空间,如果定义一个函数、全局变量各个线程都可以访问,各线程还共享以下资源:
文件描述符、每一个信号的处理方式、当前工作目录、用户id和组id

二、Linux线程控制

在Linux中,站在内核角度上看并没有真正意义上线程相关的接口。但站在用户角度,当用户想创建一个线程时更期望使用thread_create这样类似的接口,而不是vfork函数,因此系统在应用层提供了原生线程库pthread。原生线程库实际就是对轻量级进程的系统调用进行了封装,在用户层模拟实现了一套线程相关的接口

  • 用层指的是这个线程库并不是操作系统直接提供的,而是由第三方使用系统接口编写的
  • 原生指的是大部分Linux系统都会默认带上该线程库
  • 与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以"pthread_"开头
  • 要使用pthread库,要引入头文件<pthreaad.h>
  • 链接pthread库时,要在编译时要使用"-lpthread"选项

2.1线程创建

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

参数:

  • thread:获取创建成功的线程标识符,该参数是一个输出型参数
  • attr:用于设置创建线程的属性,传入NULL表示使用默认属性
  • start_routine:该参数是一个函数地址,表示线程例程,即线程启动后要执行的函数
  • arg:传给线程例程的参数(即传给start_routine的形参) 返回值:

线程创建成功返回0,失败返回错误码

使用案例

#include<iostream>
using namespace std;
#include<pthread.h>
#include<unistd.h>
void* Rountine(void* args)
{
    while(1)
    {
        cout<<"i am"<<(char*)args<<endl;
        sleep(1);
    }
}
int main()
{
    pthread_t tid;
    pthread_create(&tid,nullptr,Rountine,(void*)"thread one");
    while(1)
    {
        cout<<"i am main thread"<<endl;
        sleep(1);
    }
    return 0;
}

使用 ps -aL 命令,可以显示当前的轻量级进程,不带 -L 选项默认显示进程
在这里插入图片描述
LWP(Light Weight Process)就是轻量级进程的ID,可以看到显示的两个轻量级进程的PID是相同的,因为它们属于同一个进程

2.2线程等待

线程如同进程一般,也是需要被等待的。若主线程不对新线程进行等待,那么新线程的资源不会被回收,会发生类似于"僵尸进程"的问题,即内存泄漏。

使用pthread_join()可以进行线程等待

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

参数:

  • thread:被等待线程的标识符
  • retval:线程退出时的退出码信息

返回值:

  • 线程等待成功返回0,失败返回错误码

调用该函数的线程将阻塞到ID为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的

  • 若thread线程通过return返回,retval所指向的单元里存放的是线程的返回值
  • 若thread线程被别的线程调用pthread_cancel()异常终止掉,retval所指向的单元里存放的是宏PTHREAD_CANCELED,即(void*)-1)
  • 若thread线程是自行调用pthread_exit()终止的,retval所指向的单元存放的是传给pthread_exit的参数
  • 若对thread线程的终止状态不感兴趣,可传NULL给retval参数
    使用案例:
#include<iostream>
using namespace std;
#include<pthread.h>
#include<unistd.h>
void* Rountine(void* args)
{
    int cnt=10;
    while(1)
    {
        cnt--;
        if(cnt==0)  break;
        cout<<"i am"<< (char*)args<<endl;
        sleep(1);
    }
    return (void*)13;
}
int main()
{
    pthread_t tid;
    pthread_create(&tid,nullptr,Rountine,(void*)"thread one");
    // while(1)
    // {
    //     cout<<"i am main thread"<<endl;
    //     sleep(1);
    // }
    void* ret=nullptr;
    int n=pthread_join(tid,&ret);
    if(n==0)
    {
        cout<<"线程等待成功"<<"返回结果为"<<(long long)ret<<endl;
    }
    else    cout<<"等待失败"<<endl;
    return 0;
}

在这里插入图片描述
这里我一开始是比较疑惑为什么是void类型。其实是这样想要得到输出参数的参数类型可能为int、double、自定义类型,所以用的是void类型。

2.3线程终止

2.3.1return退出

在创建线程时指定的例程中使用return代表当前线程退出,但在main函数中使用return代表整个进程退出,即主线程退出了那么整个进程就退出了。

2.3.2 pthread_exit()

void pthread_exit(void *retval);

参数retval:线程退出时的退出信息

注意:

  • pthread_exit()或者return返回的指针所指向的内存单元必须是全局的或者堆区开辟的,不能在线程函数的栈上分配,因为当其他线程得到这个返回指针时,线程已经退出了
  • 线程退出不能使用exit()函数,其作用是退出整个进程,任何一个线程调用都是如此

2.3.3 pthread_cancel()

int pthread_cancel(pthread_t thread);

参数thread:被取消线程的标识符

返回值:线程取消成功返回0,失败返回错误码

线程是可以取消自己的(使用pthread_self()函数)。也可以让新线程取消主线程,但不建议这么使用,一般都是使用主线程去控制新线程的。

取消成功的线程的退出码一般是宏PTHREAD_CANCELED,即(void*)-1)

2.3.4 pthread_detach()

默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成内存泄漏。但若本身并不关心线程的返回值,那么join也是一种负担,此时可将该线程进行分离,后续当线程退出时就会自动释放线程资源
线程若被分离了,这个线程依旧使用该进程的资源,且依旧在该进程内运行,甚至这个线程崩溃了一定会影响整个进程,只不过这个线程退出时不再需要主线程去join了,当这个线程退出时系统会自动回收该线程所对应的资源
可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离
joinable和分离是冲突的,一个线程不能既是joinable又是分离的
使用pthread_detach()函数进程分离线程

int pthread_detach(pthread_t thread);

参数thread:被分离线程的标识符

返回值:线程分离成功返回0,失败返回错误码

2.4线程ID与进程地址空间布局

线程库NPTL提供的pthread_self()函数,获取的线程标识符和pthread_create()函数第一个参数获取的线程标识符是一样的
线程ID到底是什么?可以将线程ID打印出来看看

#include<iostream>
using namespace std;
#include<pthread.h>
#include<unistd.h>
#include<string>
string ToHex(pthread_t tid)
{
    char buf[1024];
    snprintf(buf,sizeof(buf),"%p",tid);
    return buf;
}
void* Rountine(void* args)
{
    int cnt=10;
    while(1)
    {
        cout<<(char*)args<<":"<<ToHex(pthread_self())<<endl;
        sleep(1);
    }
    return (void*)13;
}
int main()
{
    pthread_t tid;
    pthread_create(&tid,nullptr,Rountine,(void*)"thread one");
    while(1)
    {
        cout<<"main thread"<<":"<<ToHex(pthread_self())<<endl;
        sleep(2);
    }
    void* ret=nullptr;
    int n=pthread_join(tid,&ret);
    if(n==0)
    {
        cout<<"线程等待成功"<<"返回结果为"<<(long long)ret<<endl;

    }
    else    cout<<"等待失败"<<endl;
    return 0;
}

在这里插入图片描述
之前提到每个线程都有独占的栈,其中主线程采用的栈是进程地址空间中原生的栈,而其余线程采用的栈就是在共享区中开辟的。除此之外,每个线程都有各自的struct pthread,当中包含了对应线程的各种属性;每个线程还有自己的线程局部存储,当中包含了对应线程被切换时的上下文数据。

每一个新线程在共享区都有一个struct pthread对其进行描述,因此要找到一个用户级线程只需要找到该线程内存块的起始地址,然后就可以获取到该线程的各种信息
在这里插入图片描述
上面讲述的各种线程函数,本质上都是在库内部对线程属性进行的各种操作,即线程数据的管理本质是在共享区的进行的

至于pthread_t到底是什么类型取决于实现,但对于Linux目前实现的NPTL线程库来说,线程标识符本质就是进程地址空间共享区上的一个虚拟地址,同一个进程中所有的虚拟地址都是不同的,因此可以用它来唯一区分每一个线程

文章来源:https://blog.csdn.net/qq_69544758/article/details/135223009
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。