• 线程的概念与使用


    线程的概念与使用

    一、线程的概念&性质

    1、线程的概念

    线程是进程内部的一个执行流,是系统调度的最小单位

    每一个进程创建伊始都只有一个单一线程,该线程被称为主线程,执行main函数。

    补充:LWP与TCB

    Linux和一些类Unix平台是不存在真正意义上线程的概念的。以Linux为例,其内部用**轻量级进程(LWP)**对线程的调度进行模拟。

    为什么是轻量级?

    因为创建线程不需要创建独立的内存地址空间、用户级页表,只需要创建一个PCB,然后让该PCB指向进程已经创建的mm_struct即可。通过合理的资源分配,使得每一个线程都能使用进程的部分资源,因此系统对线程的调度“粒度”比进程的调度“粒度”要小很多,但是依然存在调度的消耗。

    但是对于其它平台,可能具有真正的线程。比如windows对线程就使用**Thread Control Block(TCB)**对线程进行单独的描述与组织,但这样做就使得其管理相较Linux更为复杂。

    2、线程共享的资源

    1. 进程地址空间的代码区、数据区、堆区、共享区以及环境变量和命令行参数。
    2. 进程的文件描述符表
    3. 每种信号的处理方式,包括SIG_ IGNSIG_ DFL或者自定义的信号处理函数)。
    4. 当前工作目录 。
    5. 用户id和组id。

    注:

    由于共享了文件描述符表,且线程结束不会释放fd,只有进程结束才会自动释放fd!因此某个线程打开的文件描述符一定要在该线程中关闭

    3、线程独有的资源

    1. 上下文数据,包括程序计数器、寄存器等,以方便线程的调度

    2. 独立的栈结构,以保证线程间是独立运行的。

    线程栈在Linux下为8MB,通过mmap()开辟,无法动态增长

    1. 线程ID、errno、信号屏蔽字以及线程调度的优先级。

    可以通过pthread_sigmask()函数设置线程的信号屏蔽字。

    4、线程的优缺点分析

    I. 优点

    1. 创建一个新线程的代价要比创建一个新进程小得多,因为它大部分资源是共享进程的,不需要额外创建。

    2. 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多,因为线程上下文内容较少

    3. 线程占用的资源要比进程少很多。

    4. 在等待慢速系统调用时,如read()、sleep(),进程可以切换线程以执行其他的计算任务。

    5. 对于计算密集型应用,如果有多核/多CPU系统,那么将计算分解到多个线程中实现可以加快计算速度

    6. 对于I/O密集型应用,为了提高CPU利用率,可以让部分线程等待不同的I/O操作,其他线程继续使用CPU进行运算。

    7. 线程可以看到进程的大部分资源,因此线程间的通信成本更低

    注:

    • 计算密集型也叫CPU密集型,指的是系统的硬盘、内存性能相对CPU要好很多。此时,系统运作CPU读写IO(硬盘/内存)时,IO可以在很短的时间内完成,而CPU还有许多运算要处理。

    • IO密集型指的是系统的CPU性能相对硬盘、内存要好很多。此时,系统运作,大部分的状况是CPU在等IO (硬盘/内存) 的读写操作,因此,CPU负载并不高。

    II. 缺点

    1. 编程与调试难度较高。

    2. 对临界资源进行读写时,性能降低(因为必须使用各种互斥、同步机制)。

    3. 若线程因出现除零、访问野指针等异常崩溃,整个进程也会随之崩溃。

    二、线程的使用

    1、原生线程库(NPTL)

    Linux系统没有真正的线程概念,但是用户需要操作线程,因此Linux基于轻量级进程的接口,封装了一个原生线程函数库。

    注意:使用该函数库需要在编译时加上-lpthread选项以链接函数库。

    2、线程的创建

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

    • thread:输出型参数,返回线程ID

    • attr:设置线程的属性,attr为NULL表示使用默认属性(一般设置为NULL即可)。

    • start_routine:函数地址,即该线程启动后要执行的函数(注意参数类型和返回值类型都为void*)。

    • arg:传给线程启动函数start_routine的参数(需要强转成void*类型)。

    • RetVal:成功则返回0,失败则返回错误码。

    线程ID和LWP

    在Linux下可以通过shell命令ps -aL | head -1 && ps -aL查看当前所有线程,同时显示表头。

    image-20220808144735360

    pthread_create()的输出型参数返回的值为线程ID,用于线程库根据该ID操作线程。

    同样的,通过函数:pthread_t pthread_self(void)也可以获取当前线程的ID

    但是注意:

    • 通过线程库获取到的是用户层线程ID,它的本质是一个地址。我们链接的原生线程库被存储在进程地址空间的共享区,而该线程ID就是指向库中的某一个位置,通过该位置我们可以获取到线程相关的数据

    • 通过命令行查看到的LWP,即light weight process,是OS为标识线程唯一性而设定的ID,用于CPU调度

    每创建一个线程,我们就获得它的用户层ID,同时系统也会创建它的LWP用于调度。因此,用户层线程ID和LWP是1对1的关系

    3、线程的终止

    1、通过return终止线程「推荐」
    线程启动执行的函数start_routine()的返回值是void*类型的,因此返回值内容可以自定义。

    2、通过函数终止线程「推荐」
    void pthread_exit(void *value_ptr)
    value_ptr是该进程的退出状态,相当于return的内容。

    3、通过函数终止进程
    int pthread_cancel(pthread_t thread)
    thread表示要终止的线程的ID 。函数执行成功则返回0,失败则返回错误码

    注意:

    如果线程被创建但还没被调度,那么此时取消该线程会引起程序卡死。

    此外,该函数具有一定的延时性,线程在被取消时往往还会执行一段时间,因此最好用主线程取消其余线程。

    通过该函数取消线程后,系统会自动将线程的退出码设为PTHREAD_CANCELED,即(void*)-1;

    4、通过exit(n)终止线程,但是该做法会导致整个进程退出,因为exit()函数是用来终止进程的。

    4、线程的等待

    int pthread_join(pthread_t thread, void **value_ptr)

    • thread:线程ID

    • value_ptr:输出型参数,用于获取wait成功的线程的thread_routine返回值。由于thread_routine的返回值类型为void *,因此该参数为void **。特殊情况:当线程是被pthread_cancel()中断的,则value_ptr会被置为(void *)-1

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

    该函数用于主线程阻塞式等待指定ID的线程。

    对于线程正常退出的情况,我们可以通过value_ptr获取其退出状态,如果线程发生异常而崩溃,则整个进程都会收到信号而崩溃,因此无需考虑线程异常的信号问题。

    综上所述,进程只关心线程是否跑完自己该跑的代码。如果跑完,则可以通过返回值判断线程的任务是否完成;如果没跑完,则说明发生异常,可以根据进程收到的信号进行纠错。

    I. 为什么需要线程等待

    已经退出的线程,其空间没有被释放,仍然在进程的地址空间内,且创建新的线程不会复用该地址空间。因此,不等待线程会导致系统资源泄漏

    II. 为什么不可以利用线程进行程序替换

    程序替换需要改变进程地址空间的数据和代码,因此线程调用程序替换时会导致所有当前进程的所有线程被终止。

    5、线程分离

    int pthread_detach(pthread_t thread)

    thread是要与主线程分离的线程id。

    分离的本质:主线程不关心其余线程的退出状态,让线程退出时系统自动回收资源,相当于父进程将SIGCHLD信号屏蔽。

    当一个线程被设置为分离状态,那么该线程不应该被pthread_join(),因为joinable与分离是冲突的,join一个已分离的线程必然会失败。

    但是,如果一个已分离的线程出现异常崩溃,那么整个进程依然会被影响,因为线程使用的是进程的地址空间。

    6、综合使用案例

    void* routine(void* arg)
    {
        // 线程执行5s,打印5次信息
        pthread_detach(pthread_self());
    
        int cnt = 5;
        while (cnt)
        {
            cout << "thread is running, cnt = " << cnt << endl;
            sleep(1);
            cnt--;
        }
    
        return nullptr;
    }
    
    int main()
    {
        pthread_t tid;
        pthread_create(&tid, nullptr, routine, nullptr);
        
        int ret = pthread_join(tid, nullptr);
    
        cout << "ret = " << ret << endl;
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    三、什么时候使用多线程

    • 对于计算密集型应用,如果有多核/多CPU系统,那么将计算分解到多个线程中实现可以加快计算速度

      例如超级计算机。

    • 对于I/O密集型应用,为了提高CPU利用率,可以让部分线程等待不同的I/O操作,其他线程继续使用CPU进行运算。

      例如一个基于线程并发的echo服务器,主线程不断等待连接请求,其余进程用来处理该请求。

  • 相关阅读:
    Dubbo---使用直连方式 dubbo
    C++ 炼气期之数组探幽
    GB28181状态信息报送解读及Android端国标设备接入技术实现
    力扣(LeetCode)176. 第二高的薪水(2022.06.25)
    朋友圈大佬都去读研了,这份备考书单我码住了
    INS/GPS组合导航类型简介
    重生之 SpringBoot3 入门保姆级学习(19、场景整合 CentOS7 Docker 的安装)
    本地数据库迁移到云端服务器
    TOGAF®10标准读书会首场活动圆满举办,精彩时刻回顾!
    LabelImg(目标检测标注工具)的安装与使用教程
  • 原文地址:https://blog.csdn.net/Wyf_Fj/article/details/126328658