Linux线程原理深度剖析:从CPU调度到pthread实现

一、线程概念(thread)

进程 = 资源 + 线程

线程 = 进程内部的执行分支

  1. 进程是资源的拥有者
  2. 线程是代码的执行者
  3. 一个进程至少有一个线程(主线程)
  4. 同一进程内的所有线程共享资源
  5. 线程更轻量,切换更快
  6. 进程挂了,线程全部消失
  7. CPU 只认线程,不认进程

1. CPU怎么运行代码的?

CPU 是个无脑机器,它只做一件事:

  1. 程序计数器 PC 里的内存地址
  2. 去那个地址取一条指令
  3. 执行
  4. PC 自动 +1,继续下一条

CPU完全不知道什么是程序、什么是进程、什么是线程。

只认识三样东西

  • PC(程序计数器):下一条指令在哪
  • 寄存器:当前计算的临时数据
  • 栈(stack):函数调用、局部变量

这三样东西合起来,叫:CPU 执行上下文(Context)

CPU 不调度进程,只调度 "上下文":谁有这套上下文,CPU 就运行谁。

2. 为啥线程是调度单元?

线程 = 一套独立的 CPU 上下文

换句话说:

  • 你给 CPU 一套 PC + 寄存器 + 栈
  • CPU 就能跑一段代码
  • 这个能被 CPU 调度的最小实体 ,就叫线程

(进程不携带 PC、栈、寄存器,只有线程携带)

其中,之所以能分别执行代码的一部分,是因为创建的多个PCB在代码层面上,去执行不同的函数。

3. 创建进程在干什么?

进程申请资源:提供资源空间

  • 内存
  • 代码
  • 全局变量
  • 文件
  • 打开的 socket

进程 = 地盘 / 资源容器

4. 见识Linux"线程"

进程有进程的 PCB 那线程也有自己的控制块: TCB ;在设计时,线程的控制块越设计就感觉越像进程PCB,那么此时不同的系统有了不同的设计方式。在Windows 中,线程的TCB就是单独设计 的,连接在进程下;而Linux 中,复用了"进程"PCB 内核数据结构模拟实现"线程"效果。在物理上,Linux系统中不存在真正意义上的线程!因为没有TCB

在Linux中定义:

  • 执行流 == 轻量级进程
  • 进程 == 一个或多个轻量级进程 + 其他资源

在逻辑上,Linux中不叫线程 ,只叫做轻量级进程 !!!(Light weight Process


理解POSIX线程库 :在Linux中只存在轻量级进程,它的底层调用也都是复用了进程中的,所以为了不同操作系统的统一性,将一些复用的函数,再次封装,所以就有了POSIX线程库,它是一个用户级线程库。老版本编译链接时需要加 -lpthread 选项,类似于第三方库。比如**pthread_create 创建线程**,底层调用clone(CLONE_VM)fork 创建一个新进程 ,底层也调用 clone 系统调用,(不带CLONE_VM) ;vfork 真正创建轻量级进程的接口 ,底层还是调用 clone().

**pthread(POSIX Thread)**是 Linux / Unix /macOS 系统下标准的多线程库(C 语言 / C++ 用)。

可以理解为:Linux 系统里,创建线程 = 必须用 pthread

  1. 核心特点
  • POSIX 标准线程(所有类 Unix 系统通用)
  • C 语言 / C++ 最常用的线程库
  • 编译时必须加 -pthread 参数
  • 函数全部以 pthread_ 开头

函数 pthread_creat

c 复制代码
pthread_create(
    pthread_t *thread,     // 输出:线程ID
    const pthread_attr_t *attr,  // 输入:线程属性(直接填 NULL)
    void *(*start_routine)(void *), // 输入:线程要跑的函数
    void *arg              // 输入:传给上面线程函数的参数
);
// 返回值
// 成功:返回 0
// 失败:返回错误号

void* (*start_routine)(void*)

线程要跑的函数,函数格式必须长这样:

c 复制代码
void *func(void *arg)	// 返回值:void*  参数:void*

代码示例:

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

void* threadRun(void* arg) {
    while (true) {
        std::cout << "new thread is running! pid:" << getpid() << std::endl;
        sleep(1);
    }
}

int main() {
    pthread_t tid;
    pthread_create(&tid, nullptr, threadRun, nullptr);

    while (true) {
        std::cout << "main thread is running! pid" << getpid() << std::endl;
        sleep(1);
    }
    return 0;
}

LWP 代表轻量级进程(线程)编号,与PID相同的LWP是主线程

二、深析虚拟地址空间页表

1. 物理内存的管理

2. 页表的二级分页

三、进程VS线程

进程间具有独立性;线程共享地址空间,也就共享进程资源,但也拥有自己的一部分"私有"数据:

  • 线程ID
  • 一组寄存器,线程上下文数据
  • errno
  • 信号屏蔽字
  • 调度优先级

共享的数据除了地址空间还有:

  • 文件描述符表
  • 每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
  • 当前工作目录
  • 用户id和组id

1. 线程的优点

(1)创建一个新线程的代价要比创建一个新进程小得多

(2)与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多

  • 进程切换过程伴随的最显著的性能损耗是将寄存器中的内容切换出,线程的切换虚拟内存空间依然是相同的

  • 另外一个隐藏的损耗是上下文的切换会扰乱处理器的缓存机制:

      1. 进程切换

      TLB(页表快表)全部失效刷新:进程各自拥有独立虚拟地址空间,切换进程时页目录、页表完全变更,TLB 缓存的虚拟→物理地址映射全部作废,后续内存访问频繁缺 TLB,触发慢速页表查询。

      CPU Cache(L1/L2/L3)大范围失效:新进程的代码、数据在虚拟地址上和旧进程无关联,CPU 缓存内的缓存行全部失效,缓存命中率暴跌,内存访问变慢

      1. 线程切换

      TLB 无需刷新:同进程所有线程共用一套页表,虚拟地址空间不变,TLB 保存的地址映射依旧有效。

      硬件 Cache 基本保留有效:线程共用代码段、全局数据,CPU 缓存里的缓存行大部分仍可用,不会整段清空 Cache,缓存命中率几乎不受切换影响

  • 线程占用的资源要比进程少

  • 能充分利用多处理器的可并行数量

  • 在等待慢速 I/O 操作结束的同时,程序可执行其他的计算任务

  • 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现

  • I/O 密集型应用,为了提高性能,将 I/O 操作重叠。线程可以同时等待不同的 I/O 操作

2. 线程的缺点

  • 性能损失

    • 一个很少被外部事件阻塞的计算密集型线程往往无法与其它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
  • 健壮性降低

    • 编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
  • 缺乏访问控制

    • 进程是访问控制的基本粒度,在一个线程中调用某些 OS 函数会对整个进程造成影响。
  • 编程难度提高

    • 编写与调试一个多线程程序比单线程程序困难得多

3. 线程异常

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

四、线程控制

1. POSIX 线程库

  • 与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以 pthread_ 打头
  • 使用库需引入头文件:#include <pthread.h>
  • 编译链接时,gcc/g++ 加编译选项:-lpthread

(1)pthread_create()

c 复制代码
pthread_create(
    pthread_t *thread,     // 输出:线程ID(类型是内核的,所以要提前声明变量)
    const pthread_attr_t *attr,  // 输入:线程属性(直接填 NULL)
    void *(*start_routine)(void *), // 输入:线程要跑的函数
    void *arg              // 输入:传给上面线程函数的参数
);

返回值:

  • 0:创建成功
  • 非 0 数字:错误码(不修改 errno,不能用 perror

所以出错码在返回值里,不走 errno,禁用 perror,可以用strerror(ret)来打印错误信息。

代码示例:

c 复制代码
#include <iostream>
#include <pthread.h>
#include <cstring>
#include <unistd.h>

void* Routine(void* args) {
    std::string name = static_cast<const char*>(args);  //目标类型 变量 = static_cast<目标类型>(要转换的值);
    while (true) {
        std::cout << "我是新线程:" << name << std::endl;
        sleep(1);
    }
}

int main() {
    pthread_t tid;
    int ret = pthread_create(&tid, nullptr, Routine, (void*)"thread-1");
    if (ret != 0) {
        std::cerr << "pthread_create错误:" << strerror(ret) << std::endl;
        exit(1);
    }

    printf("new thread tid: 0x%lx\n", tid);

    while (true) {
        std::cout << "我是主线程...." << std::endl;
        sleep(1);
    }

    return 0;
}

tid并不是LWP线程号

(2)pthread_self()

c 复制代码
#include <pthread.h>
pthread_t pthread_self(void);	// 返回:当前线程pthread_t ID

作用:获取【调用自己】的线程 ID,类似进程的 getpid ()

代码示例:

c 复制代码
// 修改上面的部分代码
void* Routine(void* args) {
    std::string name = static_cast<const char*>(args);  //目标类型 变量 = static_cast<目标类型>(要转换的值);
    while (true) {
        // std::cout << "我是新线程:" << name << std::endl;
        printf("我是新线程: %s, tid: ox%lx\n", name.c_str(), pthread_self());
        sleep(1);
    }
}

(补充)线程传参

pthread_create() 的第四个参数中,不是只能传入一些系统类型,还能传入任务

c 复制代码
// 文件./task.hpp
#pragma
#include <string>

class Task {
public:
    Task(int x, int y) 
        : _x(x)
        , _y(y) 
    {}
    ~Task(){}

    void Excute() {
        _result = _x + _y;
    }

    std::string Result() {
        return std::to_string(_x) + "+" + std::to_string(_y) + "=" + std::to_string(_result);
    }
private:
    int _x;
    int _y;
    int _result;
};


// 文件./code.cpp
#include <iostream>
#include <pthread.h>
#include <cstring>
#include <unistd.h>
#include "task.hpp"

void* Routine(void* args) {
    sleep(2);
    Task* t = static_cast<Task*>(args);
    t->Excute();
    std::cout << t->Result() << std::endl;
    return nullptr;

}

int main() {
    srand(time(nullptr) ^ getpid());
    // 创建多线程
    const int thread_count = 10;
    for(int i = 0; i < thread_count; ++i) {
        pthread_t tid;
        
        int x = rand() % 10 + 1;
        usleep(123);
        int y = rand() % 7 + 1;
        Task* t = new Task(x, y);
        
        int ret = pthread_create(&tid, nullptr, Routine, t);
        if (ret != 0) {
            std::cerr << "pthread_create失败:" << strerror(ret) << std::endl;
            exit(1);
        }
    }

    while (true) {
        printf("我是主线程tid: 0x%lx, pid: %d, g_val: %d, &g_val: %p\n",\
                pthread_self(), getpid(), g_val, &g_val);
        sleep(1);
    }

    return 0;
}

2. 线程终止

如果需要只终止某个线程而不终止整个进程,可以有三种方法:

  1. 从线程函数 return。这种方法对主线程不适用,从 main 函数 return 相当于调用 exit。
  2. 线程可以调用 pthread_exit 终止自己。
  3. 一个线程可以调用 pthread_cancel 终止同一进程中的另一个线程。

(1)pthread_exit()()

c 复制代码
#include <pthread.h>

void pthread_exit(void *retval);

作用:让【当前线程】退出,不影响进程里的其他线程

参数:线程退出时带回去的返回值

c 复制代码
void* Routine(void* args) {
    sleep(2);
    Task* t = static_cast<Task*>(args);
    t->Excute();
    std::cout << t->Result() << std::endl;
    
    pthread_exit(nullptr);
}

(2)pthread_cancel()

c 复制代码
#include <pthread.h>

int pthread_cancel(pthread_t thread);

作用:主动杀死 / 取消另一个线程

返回值:0 成功,非 0 失败

它怎么工作?它不是立刻暴力杀死线程,而是发一个取消请求 给目标线程,目标线程遇到取消点才会真正退出。

常见取消点

  • sleep
  • printf
  • read / write
  • pthread_join
  • 各种阻塞函数
c 复制代码
// 子线程:一直跑
void* func(void* arg)
{
    while(1)
    {
        printf("子线程运行中...\n");
        sleep(1);  // 这里是取消点!
    }
}

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

    sleep(3);  // 让子线程跑3秒

    pthread_cancel(tid);  // 主线程取消子线程

    printf("子线程已被杀死\n");
    return 0;
}
  • 被取消的线程,退出状态是 PTHREAD_CANCELED
  • 必须用 pthread_join 回收资源,否则会泄漏

3. 线程等待

为什么需要线程等待?

  • 已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。
  • 创建新的线程不会复用刚才退出线程的地址空间。

(1) pthread_join()

c 复制代码
int pthread_join(pthread_t thread, void **value_ptr);
// void **retval是输出型参数,故自声明变量

join = 加入、汇合 ,不用 "等待wait",是让主线程和子线程汇合到一起。)

参数

  • thread:线程 ID

  • value_ptr:它指向一个指针,后者指向线程的返回值

    第二个参数是 void → 表示「存储返回指针的变量的地址」,必须传 &ret,ret 是 void* 类型,使用时强转回真实类型(char* /int* 等)

返回值:成功返回 0;失败返回错误码

thread 线程以不同的方法终止,通过 pthread_join 得到的终止状态是不同的,总结如下:

  1. 如果 thread 线程通过 return 返回,value_ptr 所指向的单元里存放的是 thread 线程函数的返回值。
  2. 如果 thread 线程被别的线程调用 pthread_cancel 异常终止掉,value_ptr 所指向的单元里存放的是常数 PTHREAD_CANCELED。
  3. 如果 thread 线程是自己调用 pthread_exit 终止的,value_ptr 所指向的单元存放的是传给 pthread_exit 的参数。
  4. 如果对 thread 线程的终止状态不感兴趣,可以传 NULL 给 value_ptr 参数。
c 复制代码
#include <stdio.h>
#include <pthread.h>

void* func(void* arg) {
    pthread_exit("我是子线程返回值");
}

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

    void* ret;        // 用来接收返回指针
    pthread_join(tid, &ret);  // 传地址!

    printf("收到:%s\n", (char*)ret);
    return 0;
}

4. 线程等待

  • 默认情况下,新创建的线程 是 joinable 的,线程退出后,需要对其进行 pthread_join 操作,否则无法释放资源,从而造成系统泄漏。
  • 如果不关心 线程的返回值,join 是一种负担 ,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。

(1)pthread_detach()

c 复制代码
#include <pthread.h>
int pthread_detach(pthread_t thread);

参数:thread:目标线程 ID

返回值:成功返回 0;失败返回错误码(EINVAL 已分离、ESRCH 无对应线程

核心功能 :将默认 joinable (可连接) 线程 → detached (分离态)

  1. 线程终止后内核自动回收栈、TCB 等全部资源,无需 pthread_join,杜绝僵尸线程、资源泄漏
  2. 分离后禁止调用 pthread_join,调用直接报错,无法获取线程退出返回值

关键注意

  1. 状态不可逆:分离后不能改回 joinable,无法 join 收返回值
  2. 适用场景:启动即丢弃结果的任务(服务器临时处理线程),不需要等待返回值

两种分离方法

  1. 主线程分离子线程:pthread_detach(tid);
  2. 线程内部自分离:pthread_detach(pthread_self());

测试

1)当detach之后还join(报错):
c 复制代码
#include <iostream>
#include <pthread.h>
#include <cstring>
#include <unistd.h>
#include "task.hpp"

void* Routine(void* args) {
    int cnt = 5;
    while (cnt) {
        std::cout << "新线程在运行,次数:" << cnt-- << std::endl;
        sleep(1);
    }
    return nullptr;
}

int main() {
    Task t(10, 20);
    pthread_t tid;
    pthread_create(&tid, nullptr, Routine, (void*)&t);

    sleep(1);
    std::cout << "main 中detach分离线程" << std::endl;
    pthread_detach(tid);

    void* ret = nullptr;
    int n = pthread_join(tid, &ret);
    if (n != 0) {
        std::cerr << "join失败:" << strerror(n) << std::endl;
        exit(1);
    }
    
    std::cout << "join成功! 返回值:" << ret << std::endl;

    return 0;
}
2)线程执行内部自己detach
c 复制代码
void* Routine(void* args) {
    pthread_detach(pthread_self());	// 自己detach
    int cnt = 5;
    while (cnt) {
        std::cout << "新线程在运行,次数:" << cnt-- << std::endl;
        sleep(1);
    }
    return nullptr;
}

int main() {
    Task t(10, 20);
    pthread_t tid;
    pthread_create(&tid, nullptr, Routine, (void*)&t);

    // sleep(1);
    // 注意当此时没有sleep时,join竟然还会成功!!!因为create之后立即就join了
    // join比detach更早,所以后续线程再detach分离,后面join也会成功

    void* ret = nullptr;
    int n = pthread_join(tid, &ret);
    if (n != 0) {
        std::cerr << "join失败:" << strerror(n) << std::endl;
        exit(1);
    }
    
    std::cout << "join成功! 返回值:" << ret << std::endl;

    return 0;
}

pthread_create() 之后 sleep(1) ,就能显示正常的错误信息了

c 复制代码
void* Routine(void* args) {
    pthread_detach(pthread_self());	// 自己detach
    int cnt = 5;
    while (cnt) {
        std::cout << "新线程在运行,次数:" << cnt-- << std::endl;
        sleep(1);
    }
    return nullptr;
}

int main() {
    Task t(10, 20);
    pthread_t tid;
    pthread_create(&tid, nullptr, Routine, (void*)&t);

    sleep(1);

    void* ret = nullptr;
    int n = pthread_join(tid, &ret);
    if (n != 0) {
        std::cerr << "join失败:" << strerror(n) << std::endl;
        exit(1);
    }
    
    std::cout << "join成功! 返回值:" << ret << std::endl;

    return 0;
}
3)被分离的线程出错时,整个进程依旧会崩
c 复制代码
#include <iostream>
#include <pthread.h>
#include <cstring>
#include <unistd.h>
#include "task.hpp"


void* Routine(void* args) {
    pthread_detach(pthread_self());
    int cnt = 5;
    while (cnt) {
        std::cout << "新线程在运行,次数:" << cnt-- << std::endl;
        sleep(1);
        int a = 10;
        a /= 0;	// 除0错误
    }
    return nullptr;
}

int main() {
    Task t(10, 20);
    pthread_t tid;
    pthread_create(&tid, nullptr, Routine, (void*)&t);

    sleep(100);
    return 0;
}

五、mmap映射(补充)

mmap(memory map)是操作系统提供的核心机制 ,能把文件 / 设备 直接映射到进程的虚拟内存空间,让进程可以像读写内存一样操作文件,无需 read/write 系统调用,是高性能 IO、共享内存的核心技术。

1. 核心作用

  1. 文件高效读写 :避免传统 IO 的内核缓冲区拷贝,大文件操作性能远超 read/write
  2. 进程间共享内存:多个进程映射同一个文件,就能直接通过内存共享数据(最快的 IPC 方式)。
  3. 匿名内存分配:不关联文件,直接分配一块进程间可共享的匿名内存。
  4. 动态库加载 :Linux 加载 .so 动态库,底层都依赖 mmap

2. 设置 mmap 的接口

c 复制代码
#include <sys/mman.h>

// 映射文件/设备到内存
void *mmap(
    void *addr,       // 建议映射的虚拟地址(填NULL=系统自动分配)
    size_t length,    // 映射内存的大小(字节)(4096字节的整数倍)
    int prot,         // 内存保护模式(读/写/执行)
    int flags,        // 映射类型(共享/私有)
    int fd,           // 要映射的文件描述符
    off_t offset      // 文件偏移量(必须是页大小4096的整数倍,通常填0)(也就是从文件的哪里开始映射)
);

// 取消内存映射
int munmap(void *addr, size_t length);
// addr为mmap时的返回值,length空间大小

关键参数

  1. prot(保护模式)
    • PROT_READ:可读
    • PROT_WRITE:可写
    • PROT_EXEC:可执行
    • PROT_NONE:不可访问
  2. flags(映射类型,必选其一)
    • MAP_SHARED共享映射 → 内存修改会同步回文件,其他进程可见(进程共享、文件持久化)。
    • MAP_PRIVATE私有映射 → 内存修改不写回文件,仅当前进程可见(写时复制)。
    • 常用扩展:MAP_ANONYMOUS(匿名映射,无文件,纯内存分配)。

返回值

  • 成功:返回映射内存的起始地址(指针)。
  • 失败:返回 MAP_FAILED(即 (void*)-1)。

3. 「匿名」到底啥含义?MAP_ANONYMOUS

(1)无后端磁盘文件(匿名 = 没有绑定任何文件)

  • 普通文件 mmap:虚拟内存绑定磁盘某个文件,数据从文件加载;
  • 匿名映射:不关联任何磁盘文件,fd=-1,内存初始全 0,数据只放物理内存,进程销毁自动回收,不会落盘。

(2)包含私有 MAP_PRIVATE特性:本进程独占,别的进程访问不到这块虚拟地址。

五、线程ID与虚拟地址空间布局

1. 内核眼里的LWP VS pthread 库中的tid

2. pthread_t tid指向虚拟地址的哪里?

我们知道 pthread_t tid 本身就是一个longlong类型的地址,这个地址指向的是虚拟地址中的共享区,是pthread库pthread_create() 封装时用mmap帮我们做了事:创建线程栈 + TCB(struct pthread)。

c 复制代码
// 这是 glibc 内部代码
void *mem = mmap(
    NULL,
    栈大小(8MB) + guard页(4KB) + TCB大小,
    PROT_READ | PROT_WRITE,
    MAP_ANONYMOUS | MAP_PRIVATE | MAP_STACK,
    -1, 0
);

struct pthread *pd = 计算出 TCB 地址;
*new_thread = (pthread_t)pd;

所以,pthread_t tid 中的地址指向的是,pthread库给我们在mmap区创建的 struct_pthread (TPC)的首地址。

3. 线程栈在哪?

主线程栈

每个进程的主线程栈进程原生栈0x7ffxxxx高地址),由 OS 加载程序创建,但其中没有 mmap 配套 TCB,它的TCB而是在**动态链接器(ld-linux.so)**内部的 TLS 模板区域。

子线程栈

子线程的栈在mmap分区内,因为mmap映射时就要包括子线程栈的空间

mmap 分配整块内存 = 保护页 (guard_page) + 用户栈区 + TLS + struct pthread (TCB)

为什么主线程特殊?

  • 子线程:pthread_createglibc 自动 mmap → 栈 + TLS + TCB 一起创建
  • 主线程:内核先创建进程 → 动态链接器 ld-linux.so 启动 → 提前给主线程造好 TCB
  • 主线程从来没走过 pthread_create,所以没有 mmap 那块内存

4. 补充

(1)先 mmap、后 clone

tex 复制代码
1. 用户调用 pthread_create
↓
2. ALLOCATE_STACK → mmap一次性分配整块内存:Guard+线程栈+TLS+TCB(struct pthread)
   → 算出pd(TCB地址)→ *new_thread=(pthread_t)pd; 给用户返回tid(TCB地址)
↓
3. 全部用户态资源准备完毕 → 调用clone系统调用,向内核申请创建LWP(内核轻量级线程)

mmap = 在用户空间搭好线程的房子(栈 + TCB);clone = 去内核给房子上户口(生成 task_struct),先建房、再落户

(2)为什么一定要join

线程的栈 + TCB 是用 mmap 申请的内存,不 join 就永远不释放!会内存泄漏!

  1. 内核不会自动帮你释放吗?不会!

clone 创建的内核 LWP(内核线程)退出时,内核会自动回收内核资源

但 mmap 是用户态内存,内核不管!

  1. 那谁来释放 mmap 内存?

只有 pthread_join 会调用 munmap!

  1. 那 detach 是干嘛的?

pthread_detach,它的作用就是:告诉 glibc:线程退出时,自动 munmap,不需要我手动 join

(3)TLS线程局部存储

线程局部存储(TLS)= 每个线程自己独一份的全局变量

  • 普通全局变量:所有线程共享同一份
  • TLS 变量:每个线程有自己独立的一份,互不干扰

普通全局变量(所有线程共享)

c 复制代码
int g_count = 0;  // 所有线程共用这一个

线程 A 改 = 线程 B 也看到变化

TLS 线程局部变量(每个线程独立)

c 复制代码
__thread int t_count = 0;  // 每个线程自己一份
// __thread关键字告诉编译器:这个变量要放进「线程局部存储 TLS」里
  • 线程 A 有自己的 t_count
  • 线程 B 有自己的 t_count
  • 互相完全看不见、不干扰

经典用处!errno错误码

当某些调用出错时,会返回错误码存放在全局变量 errno 中,但是有没有想过,若多个线程同时出现一些错误,共用这一个全局变量不就会发生覆盖吗!所以系统中,给errno定义成:

c 复制代码
__thread int errno;

每个线程自己一份 errno

  • 线程 A 报错 → 改自己的 errno
  • 线程 B 报错 → 改自己的 errno
  • 互不干扰

这是 TLS 诞生的核心原因之一

每个线程有自己的日志 buffer,简单示例:

c 复制代码
#include <iostream>
#include <pthread.h>
#include <cstring>
#include <unistd.h>

__thread char buf[1024];

void* Routine(void* args) {
    int num = *(int*)args;
    snprintf(buf, sizeof(buf), "线程 %d 正在写入数据进buf...", num);
    printf("%s\n", buf);
    return nullptr;
}

int main() {
    pthread_t tid1, tid2;
    int t1 = 1;
    int t2 = 2;
    pthread_create(&tid1, nullptr, Routine, &t1);
    pthread_create(&tid2, nullptr, Routine, &t2);

    void* ret1;
    void* ret2;
    pthread_join(tid1, &ret1);
    pthread_join(tid2, &ret2);

    return 0;
}
相关推荐
A_humble_scholar1 小时前
Linux(三)深入理解 Makefile:自动变量、增量编译原理与文件时间属性
linux·服务器·c++·makefile
何中应1 小时前
Nexus如何设置端口号
java·服务器·maven·nexus
RXXW_Dor1 小时前
ModbusTcp通信C#WPF开发测试(基于Nmodbus4库应用)
服务器·网络·tcp/ip
思麟呀1 小时前
C++11并发编程:条件变量
java·linux·jvm·c++·windows
树冰之辉1 小时前
label-studio部署方式(linux版本)
linux
小此方1 小时前
Re:Linux系统篇(二十七)进程篇·十二:从零构建属于你的自定义 Shell 解释器
linux·运维·服务器
落羽的落羽1 小时前
【项目】JsonRpc框架——开发实现2(业务层)
linux·数据结构·c++·人工智能·算法·json·动态规划
Shadow(⊙o⊙)1 小时前
mkfifo()命名管道-FIFO客户端 服务端模拟。*System V消息队列、信号量(信号灯)。
linux·运维·服务器·开发语言·c++
daad7771 小时前
继续记录SITL的大循环
linux