线程概念和线程控制

文章目录

1. 线程概念

什么是线程?

举个"住房"的例子理解:

  • 传统的进程(单线程):像一个人独居
    • 他拥有整个房子(虚拟地址空间
    • 客厅、厨房、厕所(代码段、数据区、堆区)都是他一个人的
    • 他做饭、扫地、洗衣服(CPU调度执行)一件事一件事做
  • 多线程进程:多人合租
    • 资源共享: 大家住在这个房子里(共享同一个地址空间、页表、文件描述符 )。有人在客厅放个杯子(全局变量),其他人都能看见并修改
    • 独立调度: 每个租客都是独立的个体,A在看电视(线程A),B在做饭(线程B),互不干扰
    • 私有空间: 大家共用房子,每个人有自己的房间(栈空间 )和记事本(寄存器上下文)-->记住自己刚才干到哪一步了

一个进程创建伴随着进程控制块(PCB)、进程地址空间(mm_struct)以及页表的创建

每个进程都有自己独立的虚拟地址空间和独立的页表,意味着进程在运行时具有独立性

站在Linux内核角度理解:到底有没有线程?

Linux中没有真正意义上的线程

Windows VS Linux:

  • Windows:内核中真正定义了进程描述符和线程描述符两个数据结构
  • Linux:内核设计者觉得不需要这么复杂,线程本质就是共享了资源的进程,那么可以复用大量已经实现的代码

Linux的实现方式:轻量级进程(LWP)

Linux内核眼里,CPU只看到了task_struct(PCB)

  • 创建普通进程(fork):
    • 复制一个task_struct
    • 复制一张全新的虚拟地址空间(页表指向新的物理内存--->这种情况还是发生数据修改写时拷贝才做的)
    • 关系:独立、各过各的、老死不相往来
  • 创建线程(clone/pthread_create):
    • 复制一个**task_struct**
    • 但是!不复制虚拟地址空间,而是让新的**task_struct指针 指向和旧的一样的mm_struct!**
    • 关系:肉体(PCB)独立,灵魂(内存视角)共享

我们创建了3个线程,可以总结出:

  • 每个线程都是当前进程里的一个执行流
  • 线程在进程内部运行,本质是线程在进程地址空间内运行,进程申请的资源,几乎被所有线程共享

重新定义进程和线程

  • 进程是分配系统资源的基本单位--->一间房子,属于一栋楼系统的一份资源实体
  • 线程是系统进行CPU调度的基本单位--->一间房子里的每个租客

红框整个整体是进程,当然进程还有文件、信号等其他控制块---->我们之前的定义没有错:进程=内核数据结构+代码数据

总结:谈一下Linux下的进程和线程区别

从用户视角来看,线程是进程内部的执行流

但从Linux内核视角来看,其实并没有严格区分进程和线程。Linux 将它们统一实现为 task_struct ,所以CPU调度时,不关心当前调度的task_struct是进程还是线程,只关心一个一个的独立执行流

所谓的线程,在Linux内核中被称为轻量级进程 (LWP) 。它们的本质区别在于 clone 时传入的标志位不同

  • 如果是普通进程,拥有独立的虚拟地址空间 (mm_struct)
  • 如果是线程,多个 task_struct 会指向同一个 mm_struct,共享代码段、数据段和堆

所以,在Linux下,CPU调度时只看 task_struct,看到的都是一个个独立的执行流,这使得Linux的线程调度非常高效,不需要维护复杂的层级关系


2. 线程控制

创建线程

代码

cpp 复制代码
#include <iostream>
#include <string>
#include <pthread.h>
#include <sys/types.h>
#include <unistd.h>
#include <vector>

// 结构体传参
struct ThreadData{
    int thread_id;
    std::string msg;
};

std::string ToHex(pthread_t tid){
    char buf[64];
    snprintf(buf, sizeof(buf), "0x%lx", tid);
    return buf;
}

// 新线程入口函数
void* thread_routine(void* arg){
    // 参数是任意类型 按照我们需要传递的类型强转
    ThreadData* data = static_cast<ThreadData*>(arg);

    // 获取当前线程的ID
    pthread_t tid = pthread_self();

    std::cout << "新线程启动!库ID:" << ToHex(tid)
              <<" | 业务ID:" << data->thread_id
              <<" | 消息:" << data->msg << std::endl;

    sleep(3);

    std::cout << "新线程任务完成,准备退出...\n";

    std::string* result = new std::string("success");

    return (void*)result;

}

int main(){
    std::cout << "主线程开始运行, pid: " << getpid() << std::endl;

    pthread_t tid;  // 新线程ID
    // C++11列表初始化
    ThreadData* tdata = new ThreadData { 1, "hello from thread!"};
    // 参数1 线程ID指针->输出型参数
    // 参数2 线程属性->nullptr是默认属性
    // 参数3 线程函数->函数指针
    // 参数4 传给线程函数的参数->任意类型
    int ret = pthread_create(&tid, nullptr, thread_routine, (void*)tdata);
    if(ret != 0){
        std::cerr << "线程创建失败,错误码:" << ret <<std::endl;
    }

    std::cout << "主线程创建新线程成功,新线程ID: " << tid << std::endl;
    std::cout << "主线程做我的事情...\n";

    void* thread_result;
    std::cout << "主线程阻塞等待新线程结束...\n";
    pthread_join(tid, &thread_result);

    std::string* res_str = static_cast<std::string*>(thread_result);
    std::cout << "主线程等待新线程退出成功,返回值:" << *res_str << std::endl;

    delete tdata;
    delete res_str;

    return 0;
}

函数接口

cpp 复制代码
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                   void *(*start_routine) (void *), void *arg);
  • 参数:
    • thread:线程返回ID
    • attr:线程属性,nullptr表示默认属性
    • start_routine:线程启动入口函数,函数指针
    • arg:传给入口函数的参数,可以是任意类型
  • 返回值:
    • 成功返回0
    • 失败返回错误码

参数4为什么传void*

为了通用性,C语言没有模板,OS不知道要传什么类型,使用void*表示接收任何类型的参数

注意: 一定不能传栈变量的地址 ,如果主线程比子线程先结束,或者主线程进入了下一个循环,栈里的变量销毁了,子线程读到的就是垃圾数据,一定要用new在堆上开辟数据,或者确认主线程生命周期长于子线程

使用指令查看线程信息

  • LWP得到的是真正的线程ID
  • ps -aL得到线程ID后,有一个线程ID和进程ID相同,这个线程就是主线程,主线程的栈在栈区,其他线程的栈在共享区,因为pthread系列函数是pthread库提供的,动态库加载在共享区

怎么解释pthread_self()打印出来的ID?

  • 打印出来的是一个很大的数字->地址,这个ID是用户态线程库维护的ID,本质是该线程在库中元数据(线程ID、线程栈、寄存器)的内存首地址
  • 想要获取内核态的LWP ID需要gettid()

原理剖析

原理一:Linux里没有真正的线程

在Linux内核看来,线程就是进程,并没有为线程专门定义一个struct thread,依然使用struct task_struct

调用pthread_create时,底层最终调用的时clone()系统调用:

cpp 复制代码
// 伪代码演示 clone 的调用
clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, ...);

CLONE_VM (Virtual Memory):

  • 含义 :新老进程共享内存描述符 (mm_struct)。
  • 效果 :这就是为什么在主线程 new 了一个对象,子线程能直接通过指针访问!因为它们的**页表(Page Table)**指向的是同一块物理内存

CLONE_FILES:

  • 含义:共享文件描述符表。
  • 效果 :主线程打开了 log.txt,子线程直接能写,不需要重新 open
原理二:线程独有资源VS共享资源
共享资源 (体现轻量级) 独有资源 (体现独立调度)
地址空间 (代码段、数据段、堆) 栈 (Stack):每个线程必须有独立栈来保存自己的局部变量和函数调用链
文件描述符表 (打开的文件) 寄存器上下文 (PC指针、SP指针):线程切换的核心就是切寄存器
信号处理方式 (Handler) 线程 ID (LWP)
当前工作目录 信号屏蔽字 (Signal Mask)
原理三:局部性原理的应用

线程共享同一块虚拟地址空间,CPU在切换线程时,不需要切换页表(CR3寄存器不变),这意味着TLB中的缓存依然有效,Cache命中率极高

销毁线程

代码

cpp 复制代码
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <cstdlib>

using namespace std;

// 方式一 自然死亡
void* thread_return(void* arg){
    cout << "线程1 正在运行,准备通过return退出...\n";
    sleep(1);
    return (void*)10;
}

// 方式二 自杀 pthread_exit
void* thread_exit_self(void* arg){
    cout << "线程2 正在运行,准备通过phread_exit退出...\n";
    sleep(1);
    sleep(3);
    // 立即终止当前线程 后续代码不会执行
    pthread_exit((void*)20);

    cout << "线程2 这行代码不会被执行!\n";
    return nullptr;
}

// 方式三 他杀 pthread_cancel
void* thread_cancel(void* arg){
    cout << "线程3 正在死循环运行,等待被主线程取消...\n";
    while(1){
        // 这里的sleep是个取消点 只有遇到取消点 线程才会响应cancel信号
        sleep(1);
        cout << "线程3 还活着...\n";
    }
    return nullptr;
}

// 致命陷阱
void* thread_trap(void* arg){
    cout << "陷阱线程 如果我调用exit(1) 整个进程都会挂掉!\n";
    // exit(1);     // 取消注释 会发现整个程序直接退出 后面的log全没了
    return nullptr;
}

int main(){
    pthread_t tid1, tid2, tid3, tid4;
    void* ret_val;      

    pthread_create(&tid1, nullptr, thread_return, nullptr);
    pthread_create(&tid2, nullptr, thread_exit_self, nullptr);
    pthread_create(&tid3, nullptr, thread_cancel, nullptr);
    // pthread_create(&tid4, nullptr, thread_trap, nullptr);


    cout << "==============================================" << endl;
    pthread_join(tid1,&ret_val);
    cout << "主线程 等待线程1退出,返回值:" << (long long)ret_val << endl;
    pthread_join(tid2,&ret_val);
    cout << "主线程 等待线程2退出,返回值:" << (long long)ret_val << endl;
    
    cout << "主线程 请求取消线程3...\n";
    pthread_cancel(tid3);
    pthread_join(tid3,&ret_val);
    // 如果线程是被 cancel 的
    // join 得到的返回值是一个宏 PTHREAD_CANCELED (通常是 -1)
    if(ret_val == PTHREAD_CANCELED)
        cout << "主线程 线程3成功被取消...\n";
    else
        cout << "主线程 线程3异常退出...\n";

    cout << "==============================================" << endl;
    cout << "主线程 所有演示结束\n";

    return 0;

}

函数接口

pthread_exit
cpp 复制代码
#include <pthread.h>
void pthread_exit(void *retval);
  • 功能:终止调用该函数的线程
  • 细节:
    • 释放线程独有的资源,但不会释放进程共享的资源

main函数中调用pthread_exit会发生什么?

  • 直觉:main结束,进程就结束
  • 事实:如果在main结尾调用pthread_exit而不是returnexit主线程会终止,但只要有其他子线程在运行,进程依然活着
cpp 复制代码
#include <iostream>
#include <pthread.h>
#include <unistd.h>

using namespace std;

// 子线程函数:它要活得比主线程长
void* worker(void* arg) {
    for (int i = 1; i <= 5; i++) {
        sleep(1);
        cout << "[子线程] 我还在运行... 第 " << i << " 秒" << endl;
    }
    cout << "[子线程] 任务完成,我也要退出了。此时进程才会真正结束。" << endl;
    return NULL;
}

int main() {
    pthread_t tid;
    pthread_create(&tid, NULL, worker, NULL);

    cout << "[主线程] 我只负责启动子线程,现在我要先走了!" << endl;

    // 重点来了!!!
    // 如果这里写 return 0; 或者 exit(0); 子线程会瞬间被杀掉。
    // 但是我们用 pthread_exit,只结束主线程自己。
    pthread_exit(nullptr);

    // 这行代码永远不会执行
    cout << "这行代码绝对看不见!" << endl;
    
    return 0;
}

输出:

txt 复制代码
[主线程] 我只负责启动子线程,现在我要先走了!
[子线程] 我还在运行... 第 1 秒
[子线程] 我还在运行... 第 2 秒
[子线程] 我还在运行... 第 3 秒
[子线程] 我还在运行... 第 4 秒
[子线程] 我还在运行... 第 5 秒
[子线程] 任务完成,我也要退出了。此时进程才会真正结束。

原理剖析:

  • 原理一:return/exit VS pthread_exit的区别
    • return 0(main中)/exit:
      • 调用系统调用exit_group()
      • 含义:把这个进程组里的所有人全都干掉,回收内存,关闭文件
    • pthread_exit():
      • 调用系统调用exit()
      • 含义:只把当前这个调度实体的状态设为死亡,不要动共享资源(内存、文件描述符)
  • 原理二:进程退出的判断标准
    • 隶属于这个进程组的最后一个线程退出时,进程退出
    • Linux内核维护了一个计数器:
      • 程序启动,cnt = 1->主线程
      • pthread_createcnt = 2->主+子
      • 主线程调用pthread_exitcnt--->cnt = 1
        • **此时cnt不为0!内核认为进程依然活着!mm_struct**依然保留!
      • 子线程运行结束,cnt = 0
        • 内核检测到归零,彻底回收进程资源
pthread_cancel
cpp 复制代码
#include <pthread.h>
int pthread_cancel(pthread_t thread);
  • 功能:向指定线程发送终止请求
  • 底层细节(取消点)
    • 这是一个异步请求,调用了cancel,对方不一定马上死
    • 目标线程必须运行到某个系统调用 (如sleepreadwrite)时,检测到取消信号,才退出
    • 不推荐使用

return VS exit VS pthread_exit

函数 作用范围 影响
return (在线程函数中) 仅终止当前线程 最安全、最推荐的方式
pthread_exit 仅终止当前线程 可以在函数深层嵌套中直接退出线程,无需层层 return
exit() 终止整个进程 核武器 。任何一个线程调用 exit,所有线程(包括主线程)全部瞬间蒸发,内存全部回收

线程是怎么死的?

资源的释放

  • 当线程终止时,内核会将该线程的状态设置为 EXIT_ZOMBIE(僵尸状态)
  • 它的栈和 task_struct 结构体依然保留
  • 为什么要保留?因为要存返回值! 等着别人来取(Join)

pthread_join 的本质

  • pthread_join 本质上是在等待目标线程的 task_struct 变成 EXIT_ZOMBIE
  • 一旦等到,主线程会把该线程的返回值(存储在 task_struct 的某个字段里)拷贝出来,然后通知内核:"你可以把这个线程彻底回收了(释放 task_struct)"
  • 如果不 join,这块 task_struct 内存就泄露了(内存泄漏)

等待线程

代码

cpp 复制代码
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <cstdlib>

using namespace std;

// 场景1:子线程计算数据 返回一个结构体指针
struct Result{
    int code;
    string msg;
};

void* thread_work(void* arg){
    long long id = (long long)arg;
    cout << "子线程 " << id << " 开始工作...\n";
    
    sleep(2);

    // 返回值必须在堆区开辟!!!或者使用静态/全局变量
    // 绝对不能返回局部变量地址!!!函数结束栈帧就销毁了
    Result* res = new Result();
    res->code = 200;
    res->msg = "success";

    cout << "子线程 " << id << " 工作完成,返回结果地址:" << res << endl;

    return (void*)res;
}

// 场景2:子线程被异常取消
void* thread_bad(void* arg){
    cout << "子线程bad 我来打酱油,马上被cancel...\n";
    while(1) sleep(1);
    return nullptr;
}

int main(){
    pthread_t tid_work, tid_bad;

    pthread_create(&tid_work, nullptr, thread_work, nullptr);

    cout << "主线程 正在阻塞等待子线程完成...\n";

    void* ret_ptr = nullptr;
    int n = pthread_join(tid_work, &ret_ptr);
    if(n == 0){
        Result* final_res = (Result*)ret_ptr;
        cout << "主线程 成功回收工作线程!\n";
        cout << "   |--返回地址: " << ret_ptr << endl;
        cout << "   |--状态码: " << final_res->code << endl;
        cout << "   |--消息: " << final_res->msg << endl;

        delete final_res;
    }

    cout << "------------------------------------------" << endl;
    pthread_cancel(tid_bad);
    pthread_join(tid_bad, &ret_ptr);
    if(ret_ptr == PTHREAD_CANCELED)
        cout << "主线程 检测到子线程时被强制取消的!\n";

    return 0;
}

函数接口

cpp 复制代码
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
  • 功能:以阻塞等待的方式等待指定线程结束

  • 参数一:要等的哪个线程ID

  • 参数二

    • 类型void**指向void指针的指针
    • 作用:存储子线程的返回值
  • 为什么要二级指针?

    1. 子线程的函数定义是void* func(void* arg),返回一个void*(比如指向堆内存的地址0x1234
    2. 主线程想要拿到这个地址0x1234
    • 如果只传void*,同样都是一级指针,值传递,函数内部修改不了外部变量的值
    • 举个例子:想在函数内部修改一个int,需要传int*
    • 推导:而现在想要在函数内部修改一个void*,需要传void**

线程是如何阻塞和唤醒的?

  1. 调用join
    • 主线程调用 pthread_join
    • 内核检查目标线程 tid 的状态
    • 如果目标线程还在运行,主线程将自己的状态从 TASK_RUNNING 改为 TASK_INTERRUPTIBLE (可中断睡眠状态)
    • 主线程被放入一个等待队列 (Wait Queue) ,移出 CPU 运行队列。此时主线程卡死,不占用 CPU
  2. 子线程结束
    • 子线程运行完毕,调用pthread_exit
    • 内核讲子线程状态设为EXIT_ZOMBIE
    • 内核向等待队列发信号,唤醒主线程
  3. 资源回收
    • 主线程醒来,恢复TASK_RUNNING
    • 主线程读取子线程task_struct中保存的退出码->void*返回值,填入void** retal
    • 内核释放子线程的task_struct和内核栈,子线程彻底消失
相关推荐
橙露2 小时前
Linux 运维进阶:Shell 脚本自动化部署与服务器监控实战
linux·运维·服务器
橘颂TA2 小时前
【Linux 网络】从理论到实践:IP 协议的报头分析与分段技术详解
linux·运维·服务器·网络·tcp/ip
Forget_85502 小时前
RHCE第八章:防火墙
linux·服务器·数据库
海绵宝宝de派小星2 小时前
Linux内核源码结构全景解析
linux·运维·arm开发
9分钟带帽2 小时前
debain系统更新软件源
linux·debain
yayatiantian_20222 小时前
Ubuntu 24.04 安装与配置 pyenv
linux·运维·python·ubuntu·pyenv
HIT_Weston3 小时前
109、【Ubuntu】【Hugo】搭建私人博客:搜索功能(五)
linux·javascript·ubuntu
Byte不洛3 小时前
《Linux线程原理详解:进程、轻量级进程(LWP)与pthread实战》
linux·多线程
坐怀不乱杯魂3 小时前
Linux - 进程信号
linux·c++