【Linux】多线程1——线程概念与线程控制

news2024/12/26 22:51:13

文章目录

  • 1. 线程概念
    • 什么是线程
    • Linux中的线程
    • 线程的优点
    • 线程的缺点
    • 线程的独立资源和共享资源
  • 2. 线程控制
    • Linux的pthread库
    • 用户级线程


  • 📝 个人主页 :超人不会飞)
  • 📑 本文收录专栏:《Linux》
  • 💭 如果本文对您有帮助,不妨点赞、收藏、关注支持博主,我们一起进步,共同成长!

1. 线程概念

什么是线程

💭理解线程需要和进程的概念紧密联系。

  1. 线程是一个执行分支,执行粒度比进程更细,调度成本更低;
  2. 进程是分配系统资源的基本单位,线程是CPU调度的基本单位。
  3. 线程是运行在进程中的一个执行流,本质上是在进程的地址空间中运行,一个进程至少包含一个线程,称为主线程。

Linux中的线程

线程是操作系统中的抽象概念,用于实现多任务并发执行。不同的操作系统可以有不同的线程实现方法和模型。例如,在Windows操作系统中,与进程PCB对标的,构建了描述线程的数据结构 —— 线程控制块,但这样子设计有以下几个缺点:

  1. 创建线程在Windows中开销较大,因为它涉及到较多的内核资源和数据结构的分配
  2. 线程与进程无法统一组织起来
  3. 线程的调度效率低

Linux的设计者发现,线程控制块与进程控制块(PCB)大部分描述属性相同,且进程与其内部创建的线程看到的都是同一个地址空间。因此,在Linux中,线程控制块直接复用了PCB的代码,也就是说,Linux底层并没有真正的“线程”,这种复用之后的线程称之为轻量级进程

在这里插入图片描述

  • 每个轻量级进程(后面直接称为线程)都有自己的一个编号——LWP,同一个进程中的各个线程具有相同的PID。

🔎那我们之前讨论的进程是什么?这里都是轻量级进程的话,需要另有一个进程PCB来管理整个进程吗?

答案是不用。事实上,在Linux中,因为每个进程都至少有一个线程,即主线程(主执行流),这个线程的LWP和PID是相同的,因此,我们之前讨论的进程PCB,实际上就是这个主线程的task_struct。

ps -aL命令查看系统中的轻量级进程。

测试:在一个进程中,创建了10个线程,并用ps -aL命令查看。可以看到有一个主线程和10个新线程,主线程的PID和LWP相同。

在这里插入图片描述

  • 线程的调度成本低于进程,是因为同一个进程中的线程共享同一个地址空间,因此这些线程的调度只需要保存和更改一些上下文信息、CPU寄存器即可,如pc指针。而进程的调度需要修改较多的内存资源,如页表、地址空间等,而开销更大的是修改cache缓存的数据。

    cache缓存

    CPU内部的高速存储器中,保存着一些频繁访问的指令和数据,基于局部性原理,这些数据可能是未来将要被访问的,也可能是当前正在访问的。这么做的目的是减少CPU与内存的IO次数,以便快速响应CPU的请求,而不必每次都从较慢的内存中获取数据。不同进程的cache缓存数据是不同的,因此调度进程是需要切换这部分数据,而同一个进程的不同线程的cache缓存相同。

    CPU根据PID和LWP的对比,区分当前调度是线程级还是进程级,进而执行对应的调度策略。

线程的优点

  1. 线程占用的资源比进程少很多,因此创建线程的开销比创建进程小
  2. 线程的调度成本低于进程调度,线程切换时OS的工作量小
  3. 充分利用多处理器的可并行数量
  4. 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
  5. 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
  6. I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。

线程的缺点

  1. 性能损失。 一个很少被外部事件阻塞的计算密集型线程往往无法与其它线程共享同一个处理器。 如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。 例如有10个处理器,11个线程,一对一的关系被破坏后,多出来的线程就增加了额外的调度开销。
  2. 复杂性和错误难以调试。 多线程编程涉及到共享资源、并发访问和同步等问题,这增加了程序的复杂性。
  3. 健壮性降低。 编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说多线程之间是缺乏保护的。

⭕补充:

线程发生异常(如野指针、除零错误等),会导致线程崩溃,进而引发整个进程退出。从宏观角度,因为线程是进程的一个执行分支,线程干的事就是进程干的事,因此线程异常相当于进程异常,进程就会退出。从内核角度,线程出错,OS发送信号给进程,而不是单发给线程。

线程的独立资源和共享资源

进程是资源分配的基本单位,线程是调度的基本单位。一个进程中的多个线程共享线程数据,当然也有自己独立的数据。

线程的独立资源:

  • 寄存器中的上下文信息
  • 线程ID(在Linux中表现为LWP)
  • errno
  • 信号屏蔽字和未决信号集
  • 调度优先级

线程的共享资源:

  • 进程地址空间(包括进程的数据段、代码段等)
  • 文件描述符表
  • 每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
  • 当前工作目录
  • 用户ID和组ID

2. 线程控制

Linux的pthread库

Liunx中,提供给用户层进行线程控制的函数被打包在一个动态库中 —— pthread。使用线程控制接口时,需要包含头文件pthread.h,并在gcc/g++编译时加上-l pthread选项确定链接动态库。

/lib64目录下找到pthread库:

在这里插入图片描述

编译时应该添加的选项:

g++ threadTest.cc -o threadTest -l pthread # -lpthread也可以
  1. pthread_create

    功能:

    ​ 创建一个线程

    接口:

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

    参数:

    thread:线程库中定义了一个线程ID类型phtread_t,这里的thread是一个输出型参数,函数会向其指向的空间写入创建线程的ID

    attr:线程的属性,一般设为nullptr即可

    start_routine:线程执行的函数,是一个返回值类型void*,参数类型void*的函数指针

    arg:传入start_routine的参数,使用前后一般需要类型转换。

    返回值:

    RETURN VALUE
           On success, pthread_create() returns 0; on error, it returns an error number, and the contents of *thread are undefined.
    

💭关于线程退出的问题:

同子进程退出,需要父进程回收,线程也需要被另外的线程回收。回收的原因如下:1. 一个线程退出后,对应的资源不会被释放,而是留存在地址空间中。一个进程能运行的线程数是有限的,如果不加以回收,可能会导致内存泄漏!2. 一个线程退出后,其它线程可能需要获取其执行任务的结果。

  1. pthread_join

    功能:

    ​ 阻塞等待一个线程

    接口:

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

    参数:

    thread:线程ID

    retval:指向的空间中存储的是线程返回的结果(注意类型转换),因为线程函数的返回结果是void*类型,所以要用二级指针接收。如果不关心回收线程的结果,则设置为nullptr。

    返回值:

    RETURN VALUE
           On success, pthread_join() returns 0; on error, it returns an error number.
    
  2. pthread_exit

    线程函数中,可以直接用return退出线程并返回结果(可以被其它线程join接收)

    void *run(void *arg)
    {
        int cnt = 5;
        while (cnt--)
        {
            cout << "I am new thread" << endl;
            sleep(1);
        }
        return nullptr; //
    }
    

    也可以用pthread_exit函数。

    void pthread_exit(void *retval); //和return一样,返回一个void*指针
    

Linux中,线程只有joinable和unjoinable两种状态。默认情况下,线程是joinable状态,该状态下的线程退出后,占有资源不会被释放,必须等待其它线程调用pthread_join回收它,释放资源,或者进程退出,资源全部被释放。当然,可以通过调用pthread_detach分离线程,将线程设置为unjoinable状态,使其无需被等待回收,退出即被系统自动释放资源。

  1. pthread_detach

    功能:

    ​ 分离线程ID为thread的线程,使其无需被join等待。

    接口:

    int pthread_detach(pthread_t thread);
    

    返回值:

    RETURN VALUE
           On success, pthread_detach() returns 0; on error, it returns an error number.
    

线程分离可以由别的线程分离,也可以自己分离。

  1. pthread_self

    功能:

    ​ 获取当前线程的线程ID

    接口:

     pthread_t pthread_self(void);
    

测试

void *run(void *arg)
{
    int cnt = 10;
    while(cnt--)
    {
        cout << "I am new thread, cnt: " << cnt << endl;
        sleep(1);
    }
    pthread_exit(nullptr);
}

int main()
{
    cout << "I am main thread" << endl;

    pthread_t tid;
    pthread_create(&tid, nullptr, run, nullptr);

    int n = pthread_join(tid, nullptr);
    if (n != 0)
    {
        cout << "join new thread fail!!" << endl;
        exit(1);
    }
    cout << "join new thread success!!" << endl;
    return 0;
}

主线程创建新线程后,调用pthread_join会阻塞等待新线程退出。运行结果如下:

[ckf@VM-8-3-centos lesson9_thread]$ ./mythread 
I am main thread
I am new thread, cnt: 9
I am new thread, cnt: 8
I am new thread, cnt: 7
I am new thread, cnt: 6
I am new thread, cnt: 5
I am new thread, cnt: 4
I am new thread, cnt: 3
I am new thread, cnt: 2
I am new thread, cnt: 1
I am new thread, cnt: 0
join new thread success!!

可以在主线程中detach线程ID为tid的新线程,也可以在新线程中detach自己。

void *run(void *arg)
{
    //pthread_detach(pthread_self()); // 在新线程中detach自己
    int cnt = 10;
    while(cnt--)
    {
        cout << "I am new thread, cnt: " << cnt << endl;
        sleep(1);
    }
    pthread_exit(nullptr);
}

int main()
{
    cout << "I am main thread" << endl;

    pthread_t tid;
    pthread_create(&tid, nullptr, run, nullptr);

    pthread_detach(tid); // 在主线程中detach线程ID为tid的新线程
    int n = pthread_join(tid, nullptr);
    if (n != 0)
    {
        cout << "join new thread fail!!" << endl;
        exit(1);
    }
    cout << "join new thread success!!" << endl;
    return 0;
}
[ckf@VM-8-3-centos lesson9_thread]$ ./mythread 
I am main thread
join new thread fail!! #等待失败,pthread_join无法等待已分离的线程,返回值非0

如果在新线程中detach自己,可能依然能够join成功。要想成功detach线程,必须在join之前detach,因为调用pthread_join函数时,已经将线程视为joinable并阻塞等待了,此后再detach是无效的。上面代码中,如果在新线程中detach自己,由于主线程和新线程调度的先后顺序不确定性,很可能线程先join再detach,此时的detach是无效的。

  1. pthread_cancel

    功能:

    ​ 撤销(终止)一个线程ID为thread的线程

    接口:

    int pthread_cancel(pthread_t thread);
    

    返回值:

    RETURN VALUE
           On success, pthread_cancel() returns 0; on error, it returns a nonzero error number.
    

    撤销一个线程后,如果有另外的线程join该线程,那么其收到的退出结果是PTHREAD_CANCELED

    #define PTHREAD_CANCELED ((void *) -1)
    

测试

void *run(void *arg)
{
    while (true)
    {
        cout << "I am new thread" << endl;
        sleep(1);
    }
    pthread_exit(nullptr);
}

int main()
{
    cout << "I am main thread" << endl;

    pthread_t tid;
    pthread_create(&tid, nullptr, run, nullptr);

    sleep(3);
    pthread_cancel(tid);

    void *ret = nullptr;
    int n = pthread_join(tid, &ret);
    if (n != 0)
    {
        cout << "join new thread fail!!" << endl;
        exit(1);
    }
    if (ret == PTHREAD_CANCELED)
    {
        cout << "new thread is canceled" << endl;
    }
    cout << "join new thread success!!" << endl;

    return 0;
}
[ckf@VM-8-3-centos lesson9_thread]$ ./mythread 
I am main thread
I am new thread
I am new thread
I am new thread
new thread is canceled #新线程被撤销了
join new thread success!!

用户级线程

💭pthread库的线程控制接口,都不是直接操作Linux底层的轻量级进程,而是操作用户级线程。pthread库将底层的轻量级进程封装成为用户级线程,用户看到的便是线程而不是所谓的轻量级进程。动态库load到进程的共享区中,因此,用户级线程的空间也是load到进程的共享区中,线程的大部分独立资源保存在这块空间中,包括线程栈。

🔎线程库是怎么管理用户级线程的?

先描述再组织。 创建类似TCB的数据结构来描述线程,并将这些数据结构组织为一张表,如下。

在这里插入图片描述

  • 前面使用接口获取到的线程tid,其实就是该线程的用户级页表的首地址,只不过将其转换成整型的格式。

    int g_val = 100;
    
    string toHex(pthread_t tid)
    {
        char buf[64];
        snprintf(buf, sizeof(buf), "0x%x", tid);
        return string(buf);
    }
    
    void *run(void *arg)
    {
        cout << toHex(pthread_self()) << endl;
        pthread_exit(nullptr);
    }
    
    int main()
    {
        pthread_t t1;
        pthread_t t2;
    
        cout << "&g_val: " << &g_val <<endl;
        pthread_create(&t1, nullptr, run, nullptr);
        pthread_create(&t2, nullptr, run, nullptr);
    
        pthread_join(t1, nullptr);
        pthread_join(t2, nullptr);
    
        return 0;
    }
    
    [ckf@VM-8-3-centos lesson9_thread]$ ./mythread 
    &g_val: 0x6020cc #全局数据区
    0x4b30f700       #共享区
    0x4ab0e700		 #共享区
    
  • 全局变量默认是所有线程共享的,开发者需要处理多线程竞争问题。有些情况下我们需要保证一个线程独享一份数据,其它线程无法访问。这时候就要用到线程局部存储。gcc/g++编译环境中,可以__thread声明一个全局变量,从而每个线程都会独有一个该全局变量,存储在线程局部存储区中。

    __thread int g_val = 0; //__thread修饰全局变量,可以理解为从进程的全局变量变成线程的全局变量
    
    string toHex(pthread_t tid)
    {
        char buf[64];
        snprintf(buf, sizeof(buf), "0x%x", tid);
        return string(buf);
    }
    
    void *run(void *arg)
    {
        cout << "g_val: " << ++g_val << " " << "&g_val: " << &g_val << endl;
        pthread_exit(nullptr);
    }
    
    int main()
    {
        pthread_t t1;
        pthread_t t2;
        pthread_t t3;
    
        pthread_create(&t1, nullptr, run, nullptr);
        pthread_create(&t2, nullptr, run, nullptr);
        pthread_create(&t3, nullptr, run, nullptr);
        cout << "g_val: " << ++g_val << " " << "&g_val: " << &g_val << endl;
    
        pthread_join(t1, nullptr);
        pthread_join(t2, nullptr);
        pthread_join(t3, nullptr);
    
        return 0;
    }
    
    [ckf@VM-8-3-centos lesson9_thread]$ ./mythread #使用了线程局部存储
    g_val: 1 &g_val: 0x7fcb7cfcb77c
    g_val: 1 &g_val: 0x7fcb7bf366fc
    g_val: 1 &g_val: 0x7fcb7b7356fc
    g_val: 1 &g_val: 0x7fcb7af346fc
    
    [ckf@VM-8-3-centos lesson9_thread]$ ./mythread #未使用线程局部存储
    g_val: 1 &g_val: 0x6021d4
    g_val: 2 &g_val: 0x6021d4
    g_val: 3 &g_val: 0x6021d4
    g_val: 4 &g_val: 0x6021d4
    
  • 每个线程都有一个独立的栈结构,用于存储运行时的临时数据和压入函数栈帧。注意,主线程的栈就是进程地址空间中的栈。


ENDING…

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/882354.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

记一次触发器拦截更新操作

1、背景 业务上有一张表记录仓库和经纬度的&#xff0c;正常情况不怎么做变更&#xff1b;业务反馈经常出现经纬度被更新的情况&#xff0c;操作人都是接口或者admin&#xff0c;人工运维后又会被接口/admin覆盖更新掉 2、过程 遇到这种情况&#xff0c;我的第一反应是定位代…

【Kubernetes】Kubernetes的Pod控制器

Pod控制器 一、Pod 控制器的概念1. Pod 控制器及其功用2. Pod 控制器有多种类型2.1 ReplicaSet2.2 Deployment2.3 DaemonSet2.4 StatefulSet2.5 Job2.6 Cronjob 3. Pod 与控制器之间的关系 二、Pod 控制器的使用1. Deployment2. SatefulSet2.1 为什么要有headless&#xff1f;2…

.Net Core 动态加载和卸载程序集

从 .Net Core 3.0开始支持程序集的加载和卸载&#xff0c;在 .Net FrameWork中使用独立的应用程序域来实现同样的功能&#xff0c;.Net Core 不支持创建多个应用程序域&#xff0c;所以无法使用多个应用程序域来实现程序集动态加载和卸载。 AssemblyLoadContext 程序集加载上下…

Antd的日期选择器中文化配置

当你使用antd的日期选择器后&#xff0c;你会发现日期什么都是英文的&#xff1a;即便你已经在项目中配置了中文化&#xff1a; 我确实已经配置了中文化&#xff1a; 但是为啥没生效&#xff1f;官网回答&#xff1a;FAQ - Ant Design dayjs中文网&#xff1a; 安装 | Day…

宝塔部署阿里云盘webdav

安装Docker 我的系统是CentOS8&#xff0c;如果直接安装会出错&#xff0c;可以看这篇文章&#xff1a;Failed to download metadata for repo ‘appstream‘ docker 国内镜像&#xff1a; http://hub-mirror.c.163.com/下载镜像 宝塔安装docker管理器&#xff0c;然后搜索…

深入了解IP地址查询的几种方式

随着互联网的发展&#xff0c;IP地址已经成为网络世界中不可或缺的部分。而准确地了解IP地址的来源和所属地区&#xff0c;对于网络安全和数据分析等方面具有重要意义。本文将介绍几种常见的IP地址查询方式&#xff0c;帮助读者深入了解IP地址的查询方法。 WHOIS查询 WHOIS查询…

VMware 安装 Centos7

VMware 安装 Centos7 1、首先&#xff0c;下载我们需要安装的 Centos7 镜像 链接&#xff1a;https://pan.baidu.com/s/1-O5nc4OwQye_yvzTCHeyvg 提取码&#xff1a;88882、打开VMware&#xff0c;点击新建虚拟机 3、选择典型&#xff0c;点击下一步 4、选择Centos7 iso文件镜…

实验二十九、正弦波变锯齿波电路

一、题目 将峰值为 1 V 1\,\textrm V 1V、频率为 100 Hz 100\,\textrm{Hz} 100Hz 的正弦波输入电压&#xff0c;变换为峰值为 5 V 5\,\textrm V 5V、频率为 200 Hz 200\,\textrm {Hz} 200Hz 的锯齿波电压。利用 Multisim 对所设计的电路进行仿真、修改&#xff0c;直至满足…

什么是主数据管理?企业主数据管理方法论

什么是主数据管理&#xff1f; 主数据又被称为黄金数据&#xff0c;其价值高也非常重要。对企业来说&#xff0c;主数据的重要性如何强调都不为过&#xff0c;主数据治理是企业数据治理中最为重要的一环。主数据管理的内容包括 主数据管理标准、主数据应用标准 和 主数据集成服…

虚拟化和容器化

目录 一. 虚拟化和容器化的概念 什么是虚拟化、容器化 案例 为什么要虚拟化、容器化&#xff1f; 二. 虚拟化实现方式 应用程序执行环境分层 虚拟化常见类别 虚拟机 容器 JVM 之类的虚拟机 三. 常见虚拟化实现 主机虚拟化(虚拟机)实现 容器虚拟化实现 容器虚拟化实现原理 容器…

JDK19 - 虚拟线程如何进行业务代码的改造

JDK19 - 虚拟线程如何进行业务代码的改造 一. 线程池的改造二. for 循环同步代码块改造2.1 自动关闭资源会等待所有异步任务执行完毕吗&#xff1f; 一. 线程池的改造 假设我们的代码中&#xff0c;原本是这样使用线程池的&#xff1a; public static ExecutorService getThr…

套接字类型,地址族,数据序列

Socket: 1.套接字中使用的协议族 2.套接字数据传输类型信息 3.计算机间通信中所使用的协议信息 PF_INET协议族中, 符合SOCK_STREAM的只有一个: tcp 所以第三个参数为0 UDP同理 TCP套接字: 可靠的,按序传递的,基于字节的面向连接的数据传输方式的套接字 :tcp套接字数据不存在边…

前后端分离------后端创建笔记(10)用户修改

本文章转载于【SpringBootVue】全网最简单但实用的前后端分离项目实战笔记 - 前端_大菜007的博客-CSDN博客 仅用于学习和讨论&#xff0c;如有侵权请联系 源码&#xff1a;https://gitee.com/green_vegetables/x-admin-project.git 素材&#xff1a;https://pan.baidu.com/s/…

(二)结构型模式:4、组合模式(Composite Pattern)(C++实例)

目录 1、组合模式&#xff08;Composite Pattern&#xff09;含义 2、组合模式应用场景 3、组合模式的优缺点 4、组合模式的UML图学习 5、C实现组合模式的简单示例&#xff08;公司的OA系统&#xff09; 1、组合模式&#xff08;Composite Pattern&#xff09;含义 组合模…

短剧cps分销系统源码搭建 短剧cps系统开发 短剧项目怎么做

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言 一、短剧是什么&#xff1f;二、短剧项目怎么做&#xff1f;三、总结 前言 短剧分销系统&#xff1a; 对接他人短剧小程序片源&#xff0c;仅推广分销。用户看…

Docker基础概述

目录 ​编辑 一、Docker简介 二、 Docker与虚拟机的区别 1.1namespace的六项隔离 二、Docker核心概念 2.1镜像 2.2容器 2.3仓库 三、安装Docker 3.1查看 docker 版本信息 四、Docker 镜像操作 4.1搜索镜像 4.2获取镜像 4.3镜像加速下载 4.4查看镜像信息 4.5根据…

FL Studio 2023年正式更新到发布21.1新版!功能介绍

很高兴地宣布在去年12月发布重大版本更新后&#xff0c;FL Studio在2023年8月正式更新到21.1版。本次更新虽然只是维护性质&#xff0c;但我们还是为大家带来了一些全新的功能&#xff0c;包括通过钢琴卷中的音阶捕捉和自定义音符工具&#xff0c;引入更快、更有创意的音符编辑…

救生艇(力扣)贪心 JAVA

给定数组 people 。people[i]表示第 i 个人的体重 &#xff0c;船的数量不限&#xff0c;每艘船可以承载的最大重量为 limit。 每艘船最多可同时载两人&#xff0c;但条件是这些人的重量之和最多为 limit。 返回 承载所有人所需的最小船数 。 示例 1&#xff1a; 输入&#xff…

ssm实验室耗材管理系统源码和论文

ssm实验室耗材管理系统源码和论文023 开发工具&#xff1a;idea 数据库mysql5.7 数据库链接工具&#xff1a;navcat,小海豚等 技术&#xff1a;ssm 摘 要 互联网发展至今&#xff0c;无论是其理论还是技术都已经成熟&#xff0c;而且它广泛参与在社会中的方方面面。它让…

交叉编译基本概念

1 什么是交叉编译 1.1 本地编译 在解释什么是交叉编译之前&#xff0c;要先明白什么是本地编译。 本地编译可以理解为&#xff0c;在当前编译平台下&#xff0c;编译出来的程序只能放到当前的平台下运行&#xff0c;平时我们常见的软件开发都是属于本地编译。 比如我们在X8…