跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C++算法

Linux 线程管理与 POSIX 线程库实战

Linux 线程是进程内的执行流,共享地址空间但拥有独立栈和寄存器。介绍 POSIX 线程库(pthread)的核心函数如 create、join、detach,对比进程与线程的资源调度差异,讲解线程局部存储(TLS)及 C++11 std::thread 封装。内容涵盖线程生命周期管理、异常处理、上下文切换优势及内存竞争问题解决方案。

RustyLab发布于 2026/2/10更新于 2026/6/225 浏览
Linux 线程管理与 POSIX 线程库实战

1. 线程的概念

  1. 线程:是进程内部的一个执行分支(执行流)、是 CPU 调度的基本单位。
  • 由于线程是进程内部的执行分支,多个线程可以在同一进程内部并发执行,提高程序的执行效率和响应速度。
  • 由于线程是 CPU 调度的基本单位,使得 OS 能够高效地管理和分配线程的执行,提高了资源利用率和响应时间。

提示:线程在进程内部运行,本质是在进程的地址空间内运行,意味着线程可以直接访问进程地址空间。

程序的代码段包含了所有的函数和指令,每个函数在编译后都会形成一个代码块,每个代码块都有一个入口地址,这个地址是函数名的符号地址。

在单进程中,函数的调用通常是串行调用,即:一个函数调用完后才会调用另外一个函数。将代码分成多个部分,每个部分由不同的执行流(线程或进程)执行,这样可以将原本串行执行的任务变为并行执行,提高效率。

进程需要访问的资源(如:代码、数据、静态库、动态库、系统调用等)都通过地址空间来找到,每个资源在地址空间中都有对应的虚拟地址来标识和访问,所以地址空间和地址空间上的虚拟地址本质上是进程的一种'资源'。

2. 线程的理解

一、Linux、Windows 对于线程设计

  1. 创建新的线程,系统只需要创建 task_struct,不需要为新的线程分配新的地址空间和页表资源,新线程与其所属的进程共享同一份地址空间和页表,所以进程的地址空间对于线程来说是可见的,线程之间可以直接地共享数据,无需通过进程间通信 (IPC) 机制,即:同一进程内的多个线程之间共享地址空间和其他资源,这使得线程的创建和切换更加高效。

文章配图

问题:为什么 Linux 中'线程'这么设计?

简化实现、提高效率、灵活性:Linux 设计者认为,进程和线程都是执行流,具有很多的相似性(如:都需要维护上下文数据、调度等),所以没必要为线程单独设计数据结构和算法,直接复用进程的数据结构和算法,即:用进程模拟线程。

Windows 中线程的设计:系统会为线程创建 tcb(线程控制块) 结构体对象,用来描述线程相关的属性,再加其添加到特定的数据结构中。线程管理拥有一套自己的数据结构与算法。

二、进程本质概念、轻量级进程

文章配图

  1. 不要站在调度角度理解进程,而应该站在内核角度理解进程:进程是承担分配系统资源的基本实体。

例如:承担分配社会资源的基本实体是家庭,家庭中每个成员都在执行自己特定的任务,但公共的任务是让这个家庭生活变得越来越好,即:家庭是进程、家庭成员是线程。

  1. 关于调度问题:在 Linux 中,所有调度执行的执行流都被称为轻量级进程,线程也被称为轻量级进程。

3. 地址空间和页表

一、OS 管理内存、页框

  1. 页框或页帧:物理内存中一个固定大小的区域,通常大小为 4KB,它是 OS 系统进行内存管理的基本单位,用于存放数据和指令。

4KB 为内存管理和磁盘管理的基本单位。

文章配图

二、虚拟地址到物理地址的转化

  • 在 Linux 系统中,虚拟地址到物理地址的转化是通过两级页表来实现的,地址空间的大小为 4GB,虚拟地址通常被划分为三个部分:高 10 位作为页目录索引、中间 10 位作为页表索引、低 12 位作为页内偏移量。
  • 页目录的查找:使用虚拟地址的高 10 位作为页目录索引,在页目录表中查找对应的页目录项。页目录表是一个数组,其中的每一项指向页表的物理地址。
  • 页表的查找:用虚拟地址的中间 10 位作为页表索引,在对应的页表中查找对应的页表项。页表是一个数组,其中的每一项指向物理页框地址。
  • 线程要访问的代码,在内存中物理地址的计算:物理地址 = 页表项中的物理页框地址 + 页内偏移量 (虚拟地址的低 12 位)。
  • 多个执行流如何进行代码划分?

    函数编译后可以被看作一段连续的代码块,函数名作为这个代码块的入口地址。

    在链接阶段,这些函数 (代码块) 最终会被放置在程序的最终可执行文件中,链接器会将这些代码块按照一定的顺序进行排列,并分配唯一的地址范围。即:所有函数,都要按照地址空间进行统一编址,所有函数的代码块,在地址空间中都有唯一的地址范围。

    OS 通过页表和内存保护机制,确保每个进程只能访问自己有权访问的内存区域。

    不同执行流都有自己的执行起点,也就是线程函数入口地址 (虚拟地址),通过页表映射就可以找到对应物理内存的代码,从而执行相应的代码。

    4. 线程的控制

    4.1. POSIX 线程库

    1. POSIX 线程库:是 POSIX 标准中定义的线程库,提供了一套标准的线程函数。
    • 与线程有关的函数构成了一个完整的序列,绝大多数函数的名字都是以"pthread_"开头的。
    • 使用线程库函数,必须加上头文件 #include <pthread.h>,链接线程库函数时,编译器要使用"-lpthread"命令。

    Linux 中并没有为线程设计独立的结构,线程是通过轻量级进程来实现的,即:Linux 中无线程相关的系统调用,只有轻量级进程的系统调用。

    用户不知道轻量级进程这个概念,只认识进程和线程,OS 就在软件层将轻量级进程的系统调用封装成原生线程库 (pthread 库),并提供给用户熟悉的线程相关接口。

    POSIX 线程库 (pthread 库) 是在用户层实现的,不属于内核的一部分,所以 pthread 库也被称为用户级线程库。

    4.2 线程创建 — pthread_create

    文章配图

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

    1. 功能:创建新线程。
    2. 参数:thread:输出型参数,存储新线程标识符 (ID);attr:设置新线程的属性,如果不需要设置特殊属性,可以传入 NULL,NULL 表示使用默认属性;start_routine:新线程的入口函数;arg:传递给入口函数的参数。

    void*可以接收任意类型指针,可以与任意类型指针进行强制类型转换。

    1. 返回值:成功返回 0,失败返回错误码。

    传统的一些函数是,成功返回 0,失败返回 -1,并且对全局变量 errno 赋值以指示错误。pthreads 函数出错时不会设置全局变量 errno(而大部分其他 POSIX 函数会这样做),而是将错误代码通过返回值返回。pthreads 同样也提供了线程内的 errno 变量,以支持其它使用 errno 的代码。对于 pthreads 函数的错误,建议通过返回值判定,因为读取返回值要比读取线程内的 errno 变量的开销更小。

    #include <iostream>
    #include <pthread.h>
    #include <unistd.h>
    using namespace std;
    
    void* newthreadrun(void* args){
        while(true) // 新线程
        {
            cout << "I am a newthread, pid: " << getpid() << endl;
            sleep(1);
        }
        return nullptr;
    }
    
    int main(){
        pthread_t tid;
        pthread_create(&tid, nullptr, newthreadrun, nullptr); // 新线程、主线程谁先运行:不确定,由调度器决定
        while(true) // 主线程
        {
            cout << "I am a mainthread, pid: " << getpid() << endl;
            sleep(1);
        }
        return 0;
    }
    

    文章配图

    ps -aL | head -1 && ps -aL | grep xxx;

    • 功能:显示当前进程 xxx 的所有轻量级进程 (线程) 的详细信息。
    • ps -aL 是显示系统中所有进程的所有轻量级进程 (线程) 的详细信息。

    文章配图

    • 对于进程内部只有一个执行流的进程 (单进程),LWP == PID,所以 OS 使用 LWP 来进行调度。

    4.3. 获取线程 ID — pthread_self

    文章配图

    pthread_t pthread_self(void);

    • 功能:获取当前进程的标识符 (ID)。
    #include <iostream>
    #include <cstdio>
    #include <pthread.h>
    #include <unistd.h>
    #include <string>
    using namespace std;
    
    string ToHex(pthread_t id) // 10 进制转 16 进制 (地址)
    {
        char buffer[126];
        snprintf(buffer, sizeof(buffer), "0x%lx", id);
        return buffer;
    }
    
    void* newthreadrun(void* args){
        while(true) // 新线程
        {
            cout << "I am a newthread, id: " << ToHex(pthread_self()) << endl;
            sleep(1);
        }
        return nullptr;
    }
    
    int main(){
        pthread_t tid;
        pthread_create(&tid, nullptr, newthreadrun, nullptr);
        while(true) // 主线程
        {
            cout << "I am a mainthread, id: " << ToHex(pthread_self()) << endl;
            sleep(1);
        }
        return 0;
    }
    

    文章配图

    1. LWP(轻量级进程):是内核中用于标识一个执行流的唯一标识符。
    • 在大多数 OS 内核中,线程是通过轻量级进程 (LWP) 来实现的,LWP 提供了线程在内核中的表示,并且每个 LWP 都有唯一的标识符 (LWP ID)。
    1. tid:用户空间中线程唯一标识符。
    • 原生线程库维护这些标识符 tid,在创建线程时,线程库会分配唯一的 tid 给每个新线程。
    1. LWP 与 tid 关系

    一对一:用户空间中的线程都会对应内核中的 LWP,意味着每个线程在内核和用户空间都有一个唯一的标识符。在用户空间中,线程库维护一个线程 ID 的映射表,将用户空间的线程 ID 与内核中轻量级进程 (LWP) ID 关联起来。

    LWP 是由 OS 内核管理的,tid 是由线程库管理的。

    用途:LWP 在内核调度和线程管理中使用,tid 在用户空间编程和调试中使用。

    4.4. 线程终止

    1. return:线程函数执行完最后一行代码或遇到 return 语句,线程会自动终止。
    • 这种方法对于主线程不适用,因为 main 函数 return 相当于调用了 exit,exit 函数终止整个进程,会导致其他线程都被终止。

    void pthread_exit(void* retval);

    功能:显式地终止当前进程。 pthread_exit 不会自动释放资源,需要使用来回收资源。 retval 参数:线程退出时的返回值,其他线程可以通过 pthread_join 函数获取到这个返回值,如果线程没有设置返回值 (即:没有调用 pthread_exit 函数、没有 return、pthread_exit 函数的参数为 NULL),则 pthread_join 获取到的返回值是未定义的。

    文章配图

    int pthread_cancel(pthread_t thread);

    功能:用于请求终止指定线程的执行,这个请求不会立即生效,会在合适的时机终止,具体行为取决于目标线程是否设置了取消点,以及是否启用了取消状态。

    thread 参数:要终止线程的标识符 (ID)。

    返回值:成功返回 0,失败返回非 0 错误码。

    前提:在调用这个函数之前,主线程要确保目标线程创建成功并启动,如果目标线程尚未启动,调用此函数将无效,甚至可能导致未定义行为。

    文章配图

    提示:return、pthread_exit 返回的指针所指向的内存单元,必须是全局的或者 malloc 分配的,不能在线程的独立栈上分配,因为当其他线程得到这个返回指针时,线程函数已经退出了。

    4.5. 线程等待 — pthread_join

    文章配图

    问:为什么需要线程等待?

    当一个线程退出时,不会自动释放资源,它的资源 (如:线程栈、线程控制块等) 仍存放在进程的地址空间内,造成内存泄漏。

    创建新线程,不会复用刚才退出线程的资源,导致资源浪费。

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

    1. 功能:等待指定的线程终止,阻塞等待。
    2. 参数:thread:要等待的线程标识符 (ID); retval:接收被等待线程的返回值,获取此线程的执行情况。
    3. 返回值:成功返回 0,失败返回错误码。
    4. 调用 pthread_join 的线程将挂起等待,直到 id 为 thread 的线程终止,thread 线程以不同的方式终止,则 pthread_join 得到的终止状态是不同的。

    如果 thread 线程通过 return 返回,retval 所指向的单元里存放的是 thread 函数的返回值。

    如果一个线程被另一个线程调用 pthread_cancel 异常终止,retval 所指向的单元里存放的是 PTHREAD_CANCELED。

    如果 thread 线程是自己调用 pthread_exit 终止的,retval 所指向的单元里存放的是传递给 pthread_exit 的参数。

    如果 thread 线程对终止状态不感兴趣,可以传 NULL 给 retval 参数。

    #include <iostream>
    #include <cstdio>
    #include <pthread.h>
    #include <unistd.h>
    #include <string>
    using namespace std;
    
    string ToHex(pthread_t id) // 10 进制转 16 进制 (地址)
    {
        char buffer[126];
        snprintf(buffer, sizeof(buffer), "0x%lx", id);
        return buffer;
    }
    
    void* thread1(void* args){
        cout << "thread1 is running...." << endl;
        int* p1 = (int*)malloc(sizeof(int));
        *p1 = 1;
        return (void*)p1;
    }
    
    void* thread2(void* args){
        cout << "thread2 is running...." << endl;
        int* p2 = (int*)malloc(sizeof(int));
        *p2 = 2;
        pthread_exit((void*)p2);
    }
    
    void* thread3(void* args){
        while(true){
            cout << "thread3 is running..." << endl;
            sleep(1);
        }
        return nullptr;
    }
    
    int main(){
        pthread_t tid;
        void* retval = nullptr;
        pthread_create(&tid, nullptr, thread1, nullptr);
        pthread_join(tid, &retval);
        cout << "thread1 ret: " << *(int*)retval << ", thread1 id" << ToHex(pthread_self()) << endl;
        
        pthread_create(&tid, nullptr, thread2, nullptr);
        pthread_join(tid, &retval);
        cout << "thread2 ret: " << *(int*)retval << ", thread2 id" << ToHex(pthread_self()) << endl;
        
        pthread_create(&tid, nullptr, thread3, nullptr);
        pthread_cancel(tid);
        pthread_join(tid, &retval);
        if(retval == PTHREAD_CANCELED) 
            cout << "thread3 return, thread3 id: " << ToHex(pthread_self()) << ", return code: PTHREAD_CANCELED\n" << endl;
        else 
            cout << "thread3 return, thread3 id: " << ToHex(pthread_self()) << ", return code: NULL" << endl;
        return 0;
    }
    

    文章配图

    pthread_join 不考虑线程异常情况,因为它会导致整个进程立即退出,pthread_join 无法拿到子线程退出情况。

    4.6. 线程分离 — pthread_detach

    int pthread_detach(pthread_t thread);

    1. 功能:分离一个进程。
    2. 返回值:成功返回 0,失败返回错误码。
    3. 默认情况下,新建的线程是 joinable 的,线程退出后,必须对其进行 pthread_join 操作,否则无法释放资源,从而造成内存泄漏。
    4. 如果主线程不关心新线程的执行情况 (即:返回值),join 是一种负担,新线程可以被设置为分离状态,则主线程不需要等待新线程完成,新线程在完成其工作后会自动释放其资源,不需要使用 pthread_join 来回收资源,如果使用了 pthread_join 会报错,因为无法获取其退出状态。
    5. 可以是线程组内其他线程对目标线程进行分离:pthread_detach(pthread_t thread)、也可以是线程自己分离:pthread_detach(pthread_self())。
    6. 线程被创建后,无论是否分离,它都会运行在主线程所在的地址空间中,这意味着新线程和主线程仍共享相同的地址空间。如果主线程退出,则分离后的线程也会退出。即:分离后的线程仅仅不需要主线程 join,其他都与线程的特性保持一致。

    文章配图

    5. 线程的特点

    5.1. 优点

    1. 创建角度 — 创建一个新线程的代价要比创建一个新进程小得多。
    2. 调度角度 — 与进程之间的切换相比,线程之间的切换需要 OS 做的工作要少很多。
    3. 释放资源角度 — 线程占用的资源要比进程少得多。
    4. 能充分利用多处理器的可并行数量。
    5. 在等待 I/O 操作结束的同时,程序可执行其他计算任务。
    6. 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现。
    7. I/O 密集型应用,为了提高性能,将 I/O 操作重叠。线程可以同时等待不同的 I/O 操作。

    问:为什么线程切换在调度角度上优势比进程切换更为明显——面试题?

    一、资源共享

    1. 每个进程都有独立的地址空间、系统资源 (如:文件、设备、信号处理器等)。进程间通信需要借助 IPC 机制,这增加了通信的复杂性和开销。在进行进程切换时,OS 需要保存和恢复整个进程的状态,包括内存管理信息、文件描述符、信号处理状态等,这进一步增加了切换的开销。
    2. 由于线程共享地址空间、系统资源、内存布局,因此通信开销较小。在进行线程切换时,OS 无需保存和恢复整个进程的状态,只需要保存和恢复线程的私有数据 (如:寄存器、栈指针等)。

    二、上下文切换

    1. 进程上下文切换:包括 CPU 上下文切换 (如:寄存器、程序计数器等)、内存上下文切换 (如:页表、文件描述符表等),所以在进行进程切换时,OS 既需要保存和恢复进程的执行上下文 (寄存器、程序计数器等)、还需要重新加载页表、文件描述符表等,这进一步增加了切换的复杂性。
    2. 线程上下文切换:由于线程共享地址空间、内存布局,不涉及内存上下文的切换。在进行线程切换时,OS 只需要保存和恢复线程的执行上下文 (寄存器、程序计数器等)、不需要重新加载页表、文件描述符表等,大大减少了切换的开销。

    三、局部性原理 (主要问题)

    1. 进程:由于每个进程有自己的独立地址空间、内存布局,缓存中的数据在进程切换时通常会失效,需要更换当前正在使用的缓存内容,这会导致缓存命中率下降,CPU 需要重新从主存储器中加载代码、数据到缓存中,增加了延迟和开销。
    2. 线程:由于线程共享地址空间、内存布局,缓存中的数据在线程切换时通常不会被丢弃,不需要更换当前正在使用的内容,线程使用的代码和数据很可能仍然在缓存中,CPU 可以立即使用缓存中的数据执行指令,而无需等待从主存储器中加载数据,这提高了缓存的命中率、CPU 执行速度。

    CPU 上集成了硬件级别的缓存 (Cache L1~L3),其工作原理如下:

    缓存的设计基于局部性原理 (即:程序在运行时倾向于最近访问的数据和指令),有两种类型,时间局部性是指如果某个数据被访问了一次,近期它可能会被再次访问,空间局部性是指如果某个数据被访问了,那么其附近的数据也有可能被访问。

    缓存通常以缓存行为单位进行读写。当 CPU 访问某个内存地址时,它可能会将整个地址所在的缓存行加载到缓存中,有助于利用空间局部性,因为相邻的内存地址往往被一起访问。

    缓存替换策略:当缓存满了而需要加载新数据时,此策略会决定哪些数据应该被丢弃。

    5.2. 缺点

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

    5.3. 线程异常

    1. 多线程中,任何一个线程出了异常 (除零、野指针等),都会导致整个进程退出。— 从而得知多线程代码往往健壮性不好。线程安全问题。
    2. 主线程退出 → 进程退出 → 所有线程退出。所以往往我们需要主线程最后退出。
    3. 总结:线程是进程的执行分支,线程出异常,就类似于进程出异常,进而触发信号机制,终止进程,则该进程内所有的线程也就随即退出。
    4. 多线程中,公共函数如果被多个进程同时进入,则该函数被重入了。
    #include <iostream>
    #include <cstdio>
    #include <pthread.h>
    #include <unistd.h>
    #include <string>
    using namespace std;
    
    string ToHex(pthread_t id) // 10 进制转 16 进制 (地址)
    {
        char buffer[126];
        snprintf(buffer, sizeof(buffer), "0x%lx", id);
        return buffer;
    }
    
    void* newthreadrun(void* args) // 子线程
    {
        int cnt = 3;
        while(cnt--){
            cout << "I am a newthread, id: " << ToHex(pthread_self()) << endl;
            sleep(1);
        }
        int* p = nullptr; // 野指针异常
        *p = 4;
        return nullptr;
    }
    
    int main(){
        pthread_t tid;
        pthread_create(&tid, nullptr, newthreadrun, nullptr);
        while(true) // 主线程
        {
            cout << "I am a mainthread, id: " << ToHex(pthread_self()) << endl;
            sleep(1);
        }
        return 0;
    }
    

    文章配图

    6. 进程 VS 线程

    1. 进程是资源分配的基本单位,线程是调度的基本单位。
    2. 尽管线程之间共享数据,但线程也有私有数据。
    • 硬件上下文 (一组寄存器) —— 调度
    • 线程栈 —— 常规运行
    • 线程 ID
    • errno
    • 调度优先级
    • 信号屏蔽字

    线程栈:是一个独立的栈结构,用于存储线程执行时的局部变量、函数参数、返回地址、调用栈等信息。

    1. 进程的多个线程共享同一地址空间,所以代码段 (Text Segment)、数据段 (Data Segment) 是共享的。
    • 代码和全局数据 (全局函数、全局变量)
    • 文件描述符表
    • 每种信号的处理方式 (信号的 handler 表)
    • 当前工作目录 pwd
    • 用户 id 和组 id

    文章配图

    #include <iostream>
    #include <cstdio>
    #include <pthread.h>
    #include <unistd.h>
    #include <string>
    using namespace std;
    
    // 全局成员变量
    int g_val = 3;
    
    // 全局成员函数
    string ToHex(pthread_t id) // 10 进制转 16 进制 (地址)
    {
        char buffer[126];
        snprintf(buffer, sizeof(buffer), "0x%lx", id);
        return buffer;
    }
    
    void* newthreadrun(void* args) // 子线程
    {
        while(true){
            cout << "newthread id: " << ToHex(pthread_self()) << ", g_val: " << g_val << ", &g_val: " << &g_val << endl;
            g_val--;
            if(g_val == 0) break;
            sleep(1);
        }
        return nullptr;
    }
    
    int main(){
        pthread_t tid;
        pthread_create(&tid, nullptr, newthreadrun, nullptr);
        while(true) // 主线程
        {
            cout << "mainthread id: " << ToHex(pthread_self()) << ", g_val: " << g_val << ", &g_val: " << &g_val << endl;
            if(g_val == 0) break;
            sleep(1);
        }
        return 0;
    }
    

    文章配图

    7. 线程管理

    7.1. 线程 ID 的本质

    1. 线程 ID 定义:线程 ID 是 OS 用于唯一标识一个线程的标识符。
    • 在 POSIX 线程库 (pthreads) 中,线程 ID 的类型为 pthread_t。
    1. 线程管理:OS 为了管理进程,Linux 内核设计了一套专用于进程的数据结构、算法,Linux 为了系统的简洁性和轻量性,并没有为线程设计一套新的数据结构和算法,而是复用了进程的数据结构和算法。
    2. 线程库通常是一个动态库,通过 ldd 命令可以看得到,进程运行时,动态库需要被加载到内存中,然后通过页表映射到地址空间中的共享区,地址空间的共享区可以被进程内所有的线程访问到。

    文章配图

    图

    1. 线程库:提供了管理线程的一系列接口函数,实现了描述线程的数据结构和一些管理工作,即:对于线程的管理工作,由线程库来完成。

    图

    1. 线程控制块 (TCB):每个线程都有自己的 TCB,包含对应线程的各种属性和状态信息。
    2. 线程栈:每个线程都有自己私有的独立栈,主线程采用的是进程地址空间中的原生栈,而其余的线程采用的是共享区中的线程库中的栈。
    3. 线程 ID 的本质:在 NPTL 线程库中,线程 ID 本质是一个指向线程控制块 (TCB) 的指针,这个指针指向共享区中的一个内存块,这个内存块 (TCB) 的起始地址就是线程 ID。

    同一个进程中所有虚拟地址都是不同的,因此可以根据虚拟地址来区分每一个线程,线程的后续操作,就是根据线程 ID 来进行操作的。

    问:pthread_t 到底是什么类型呢?

    取决于实现。对于 Linux 目前实现的 NPTL 实现而言,pthread_t 类型的线程 ID,本质就是地址空间上的一个地址。

    7.2. 线程局部存储 (TLS)

    1. 线程局部存储:是一种机制,允许每个线程拥有自己变量的副本,这些变量在每个线程中独立存在、互不影响。这种机制确保了线程数据的独立性,从而避免了全局变量或静态变量在并发环境下竞态条件和数据不一致的问题。
    2. 线程局部存储的优点:数据隔离、减少了同步开销、提高了性能。
    3. __thread 关键字:用于声明线程局部存储变量,使用 __thread 关键字声明的变量在每个线程中都有一个独立的副本,这些副本互不影响,有助于避免线程间的竞态条件或数据不一致问题,提高线程安全性。

    __thread 数据类型 变量名 ;

    #include <iostream>
    #include <cstdio>
    #include <pthread.h>
    #include <unistd.h>
    #include <string>
    using namespace std;
    
    __thread int g_val = 3; // 线性局部存储变量
    
    string ToHex(pthread_t id) // 10 进制转 16 进制 (地址)
    {
        char buffer[126];
        snprintf(buffer, sizeof(buffer), "0x%lx", id);
        return buffer;
    }
    
    void* newthreadrun(void* args) // 子线程
    {
        while(true){
            cout << "newthread id: " << ToHex(pthread_self()) << ", g_val: " << g_val << ", &g_val: " << &g_val << endl;
            g_val--;
            if(g_val == 0) break;
            sleep(1);
        }
        return nullptr;
    }
    
    int main(){
        pthread_t tid;
        pthread_create(&tid, nullptr, newthreadrun, nullptr);
        while(true) // 主线程
        {
            cout << "mainthread id: " << ToHex(pthread_self()) << ", g_val: " << g_val << ", &g_val: " << &g_val << endl;
            if(g_val == 0) break;
            sleep(1);
        }
        return 0;
    }
    

    文章配图

    8. 实现多线程任务

    1. 在多线程中,线程函数的参数和返回值,我们可以传递 / 返回 基本数据类型 (int、float、char)、指针、结构体对象、自定义类型对象作为参数 / 返回值。参数通常需要通过指针传递 / 返回值通常通过 void* 类型的指针返回。
    #include <iostream>
    #include <cstdio>
    #include <pthread.h>
    #include <unistd.h>
    #include <string>
    #include <vector>
    using namespace std;
    
    void* threadname(void* args) // 参数为基本类型
    {
        const string& name = static_cast<char*>(args);
        while(true){
            cout << "I am " << name << endl;
            sleep(2);
        }
        return nullptr;
    }
    
    int main(){
        vector<pthread_t> threads;
        for(int i = 1; i <= 5; i++) // 多线程创建
        {
            // 错误代码,多线程共享同一块内存区域 (缓冲区)
            char buffer[64];
            snprintf(buffer, 64, "Thread-%d", i);
            pthread_t tid;
            pthread_create(&tid, nullptr, threadname, buffer);
            threads.push_back(tid);
        }
        for(auto& e : threads) // 等待多线程
            pthread_join(e, nullptr);
    }
    

    文章配图

    问:为什么创建的多线程名字都相同呢?

    答:buffer 缓冲区,用于存储线程的名字,在创建线程时传递这个缓冲区的地址给线程,因为所有线程共享这块缓冲区,那么在主线程创建新线程的过程中,缓冲区的内容会被不断修改,导致前面创建的线程读取到错误的数据。

    在多线程编程中,如果多个线程共享同一块内存区域 (如缓冲区),会导致数据竞争和不一致的问题,为了确保每个线程能够正确访问和修改自己的数据,应该为每个线程分配独立的内存区域 (如堆空间)。

    #include <cstdio>
    #include <pthread.h>
    #include <unistd.h>
    #include <string>
    #include <vector>
    using namespace std;
    
    void* threadname(void* args) // 参数为基本类型
    {
        const string& name = static_cast<char*>(args);
        while(true){
            cout << "I am " << name << endl;
            sleep(2);
        }
        return nullptr;
    }
    
    int main(){
        vector<pthread_t> threads;
        for(int i = 1; i <= 5; i++) // 多线程创建
        {
            // 为每个线程分配独立的内存区域,防止出现数据竞争和不一致问题
            char* buffer = new char[64]; // char buffer[64];
            snprintf(buffer, 64, "Thread-%d", i);
            pthread_t tid;
            pthread_create(&tid, nullptr, threadname, buffer);
            threads.push_back(tid);
        }
        for(auto& e : threads) // 等待多线程
            pthread_join(e, nullptr);
    }
    

    文章配图

    #include <iostream>
    #include <cstdio>
    #include <pthread.h>
    #include <vector>
    #include <string>
    #include <unistd.h>
    #include <cstdlib>
    using namespace std;
    
    #define NUM 5
    
    // 任务类
    class Task {
    public:
        Task(){}
        ~Task(){}
        void SetData(int x, int y){ _x = x; _y = y;}
        int Add(){ return _x + _y;}
    private:
        int _x;
        int _y;
    };
    
    // 线程类
    class Thread {
    public:
        Thread(int x, int y, const string& threadname):_threadname(threadname){ _t.SetData(x, y);}
        string& Threadname(){ return _threadname;}
        int Run(){ return _t.Add();}
        ~Thread(){}
    private:
        string _threadname; // 名字
        Task _t; // 任务
    };
    
    // 任务结果类
    class Result {
    public:
        Result(const string& threadname, int result):_threadname(threadname),_result(result){}
        void print(){ cout << _threadname << ": " << _result << endl;}
    private:
        string _threadname;
        int _result;
    };
    
    void* handerTask(void* args){
        Thread* td = static_cast<Thread*>(args); // 类型转换
        const string& threadname = td->Threadname();
        int result = td->Run();
        Result* res = new Result(threadname, result);
        return res;
    }
    
    int main(){
        vector<pthread_t> id;
        vector<Result*> res;
        for(int i = 1; i <= NUM; i++){
            char threadname[128];
            snprintf(threadname, sizeof(threadname), "Thread-%d", i);
            Thread* td = new Thread(10, 20, threadname);
            pthread_t tid; // 创建新线程,参数和返回值为自定义类对象
            pthread_create(&tid, nullptr, handerTask, td);
            id.push_back(tid);
        }
        for(auto& e : id){
            void* tmp = nullptr;
            pthread_join(e, &tmp); // 获取新线程的返回值 (执行情况)
            res.push_back((Result*)tmp);
        }
        for(auto& e : res){
            e->print();
            delete e;
        }
        return 0;
    }
    

    文章配图

    9. C++11 线程的封装

    1. C++11 通过引入头文件,为开发者提供了一套统一且高效的线程 API。
    • 在编译和链接的时候通常需要加上 -pthread 选项,告诉编译器你需要链接 pthread 库。
    1. std::thread 类是 C++ 标准库中用于创建和管理线程的核心类,它对底层线程 API 进行了抽象和封装,使得开发者无需关注特定 OS 的细节,只需要使用 C++ 标准接口就可以创建和控制线程。这种封装方式提高了 C++ 程序的跨平台性和可移植性。

    跨平台性:C++ 多线程库在不同的 OS 上提供了统一的接口。意味着可以在不同的平台编写相同的多线程代码,而不需要关心底层的具体实现。

    封装:C++ 多线程库封装了底层的 OS 线程 API,如:在 Linux 中,使用的是 POSIX 线程 (pthread 库),pthread 库是 Linux 底层提供多线程的常用方式。在 Windows 中,多线程的实现方式是对线程调用接口进行了封装。

    #include <iostream>
    /* <thread> 库的实现依赖于底层的线程库,在编译和链接的时候通常需要加上 -pthread 选项,告诉编译器你需要链接 pthread 库 */
    #include <thread> // 定义了与多线程编程相关的类和函数
    #include <unistd.h>
    using namespace std;
    
    // 线程函数
    void threadFunction(int num){
        while(true){
            cout << "I am newthread " << num << endl;
            sleep(1);
        }
    }
    
    int main(){
        const int num = 5;
        // thread 类,创建和管理线程
        thread t(threadFunction, num);
        while(true){
            cout << "I am mainthread" << endl;
            sleep(1);
        }
        // thread 类提供了 join、detach 成员函数来管理线程的声明周期
        t.join();
        return 0;
    }
    

    文章配图

    文章配图

    #include <iostream>
    #include <cstdio>
    #include <pthread.h>
    #include <string>
    #include <functional>
    using namespace std;
    
    namespace zzx {
        template<typename T>
        using fun_c = function<void(T)>;
    
        template<typename T>
        class thread {
        public:
            thread(fun_c<T> func, T data, const string& name = "thread none-name")
                :_func(func),_data(data),_name(name),_stop(true){}
            ~thread(){}
    
            // 注意:类成员函数,默认第一个参数为 this 指针,静态成员函数无 this 指针
            static void* threadroutine(void* args){
                thread<T>* td = static_cast<thread<T>*>(args); // 强制类型转换
                td->_func(td->_data);
                return nullptr;
            }
    
            bool start(){
                // 为了在静态成员函数 threadroutine 中访问成员变量,参数传递类对象指针 (this)
                int n = pthread_create(&_tid, nullptr, threadroutine, this);
                if(n != 0) return false;
                _stop = false;
                return true;
            }
    
            void detach(){
                if(!_stop) pthread_detach(_tid);
            }
    
            void join(){
                if(!_stop) pthread_join(_tid, nullptr);
            }
    
            string name(){ return _name;}
    
            void stop(){ _stop = true;}
        private:
            pthread_t _tid;
            string _name;
            T _data;
            fun_c<T> _func;
            bool _stop;
        };
    }
    
    #include <vector>
    #include <unistd.h>
    #include <cstdio>
    #include <string>
    #include <functional>
    #include <iostream>
    
    #define NUM 5
    using namespace zzx;
    
    void route(int num){
        while(num){
            cout << "I am newthread, num: " << num << endl;
            num--;
            sleep(1);
        }
    }
    
    int main(){
        vector<thread<int>> threads; // 创建一批线程
        for(int i = 1; i <= NUM; i++){
            // 堆空间,防止线程共享同一块内存区域,造成数据竞争和不一致问题
            char* buffer = new char[64];
            snprintf(buffer, 128, "thread-%d", i);
            threads.emplace_back(route, 5, buffer);
        }
        // 启动一批线程
        for(auto& e : threads) e.start();
        // 等待一批线程
        for(auto& e : threads){
            e.join();
            cout << "wait thread done, thread is " << e.name() << endl;
            sleep(1);
        }
        return 0;
    }
    

    文章配图

    目录

    1. 1. 线程的概念
    2. 2. 线程的理解
    3. 一、Linux、Windows 对于线程设计
    4. 二、进程本质概念、轻量级进程
    5. 3. 地址空间和页表
    6. 一、OS 管理内存、页框
    7. 二、虚拟地址到物理地址的转化
    8. 4. 线程的控制
    9. 4.1. POSIX 线程库
    10. 4.2 线程创建 — pthread_create
    11. 4.3. 获取线程 ID — pthread_self
    12. 4.4. 线程终止
    13. 4.5. 线程等待 — pthread_join
    14. 4.6. 线程分离 — pthread_detach
    15. 5. 线程的特点
    16. 5.1. 优点
    17. 一、资源共享
    18. 二、上下文切换
    19. 三、局部性原理 (主要问题)
    20. 5.2. 缺点
    21. 5.3. 线程异常
    22. 6. 进程 VS 线程
    23. 7. 线程管理
    24. 7.1. 线程 ID 的本质
    25. 7.2. 线程局部存储 (TLS)
    26. 8. 实现多线程任务
    27. 9. C++11 线程的封装
    • 💰 8折买阿里云服务器限时8折了解详情
    • Magick API 一键接入全球大模型注册送1000万token查看
    • 🤖 一键搭建Deepseek满血版了解详情
    • 一键打造专属AI 智能体了解详情
    极客日志微信公众号二维码

    微信扫一扫,关注极客日志

    微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

    更多推荐文章

    查看全部
    • Qwen3-Embedding-4B 本地化部署实战:llama.cpp 与 vLLM 方案
    • CogVideoX v1.5 开源发布:支持 5/10 秒视频生成与图生视频增强
    • PostgreSQL 动态分区裁剪技术:查询性能优化解析
    • Docker 部署 music-tag-web 音乐标签编辑器
    • Python 爬虫实战:爬取网易云热歌榜歌曲
    • 二叉搜索树:概念、性能与实现
    • C++ 语法基础:STL、位运算与常用库函数
    • Windows 系统安装 Neo4j 图数据库指南
    • Figma 搭配 Claude 与 Weavy AI:从原型到素材的完整工作流
    • 2026 年国家自然科学基金 AI 使用声明撰写指南
    • Fish Speech 1.5 结合 Whisper 打造语音处理自动化闭环
    • 前端开发常用开源 JavaScript 库、框架与工具
    • SQL Server 到 KingbaseES V9R4C12 的零改造迁移实战
    • 2022 互联网春招备战:Android 开发核心面试题与优化指南
    • MyBatis 扫描路径配置错误导致 Mapper 未找到问题排查
    • C++ 继承:面向对象代码复用的核心机制
    • OpenClaw 集成 QVeris 实现 AI 实时数据查询
    • AI 绘画模型加载报错修复指南
    • 2023 中国大模型落地应用案例解析:技术、趋势与生态
    • Stable Diffusion 秋叶整合包安装与基础使用

    相关免费在线工具

    • 加密/解密文本

      使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

    • Gemini 图片去水印

      基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online

    • Base64 字符串编码/解码

      将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

    • Base64 文件转换器

      将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

    • Markdown转HTML

      将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

    • HTML转Markdown

      将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online