• 忘掉天地
  • 仿佛也想不起自己
bingliaolongBingliaolong  2026-07-28 12:38 Aet 隐藏边栏 |   抢沙发  1 
文章评分 1 次,平均分 5.0

线程

概述

  1. 一个线程不仅仅是一段正在执行的代码,而是下面几部分的集合
    1. CPU 执行现场
    2. 内核调度信息
    3. 用户态线程环境
    4. 线程私有数据

WindowsLinux

  1. LinuxWindows 的整体模型非常相似
    1. 同一进程内的线程共享进程资源,但每个线程拥有独立的执行现场、栈、调度状态、TLS、信号状态和部分运行库状态
  2. 不过 Linux 有一个很有特色的地方
    1. Linux 内核看来,进程和线程本质上都是可调度任务,主要区别是它们共享了多少资源

Windows线程

Windows 为每个线程维护了什么

层次 主要内容 用途
CPU 执行上下文 寄存器、指令地址、栈指针 线程切换后继续执行
内核线程对象 状态、优先级、时间片、等待信息 调度线程
用户态线程环境 TEB、错误码、TLS 槽位等 Win32 运行环境
程序自己的线程私有数据 thread_localTLS/FLS 数据 每个线程保存独立状态

CPU 执行上下文

  1. 线程被切换出去时,Windows 必须保存它的执行现场,否则下次不知道从哪里继续
  2. 其中包含:
    1. 指令指针:下一条执行什么指令
    2. 栈指针:当前线程栈的位置
    3. 通用寄存器
    4. 浮点和 SIMD 寄存器
    5. 调试寄存器
    6. 处理器状态标志

上下文切换

  1. 注意,寄存器并不是永远属于某个线程
    1. 线程正在运行时使用 CPU 的真实寄存器
    2. 被切换出去时,寄存器值会被保存到该线程的上下文记录中

每个线程都有自己的线程栈

  1. 同一进程中的线程共享代码和堆,但每个线程都有独立栈
  2. 如果两个线程同时调用 func(),两个线程分别拥有自己的 value

  1. 线程栈中通常保存:
    1. 局部变量
    2. 函数参数
    3. 返回地址
    4. 保存的寄存器
    5. 临时对象
    6. 异常处理相关信息
  2. Windows 线程栈通常会先保留一块较大的虚拟地址空间,只提交实际使用的部分
    1. 接近边界时,通过保护页逐步增长
    2. 栈大小也可以在创建线程或链接程序时配置

TEB:线程的用户态档案

  1. Windows 在用户态为每个线程维护一个重要结构:

  1. TEB 中包含或关联的信息包括:
    1. 当前线程栈的范围
    2. TLS 槽位
    3. 前线程的 Win32 last-error
    4. 线程 ID 等客户端信息
    5. 异常处理、调试及运行库相关状态
    6. 指向进程环境块 PEB 的指针
  2. 访问TEB
    1. x64 Windows 中,通常可以通过 GS 段相关机制快速访问当前线程的 TEB
    2. x86 中传统上使用 FS

GetLastError 为什么线程独立

  1. 因为错误码保存在当前线程相关的环境中,而不是一个进程级全局变量里

线程本地存储 TLS

  1. Thread Local Storage
  2. 它解决的问题是:
    1. 使用同一个变量标识,但让每个线程得到一份独立的数据
  3. 普通全局变量:
    1. 所有线程访问的是同一个对象

  1. 线程局部变量:
    1. 每个线程拥有一份

TLS 底层

  1. 先用一个简化模型理解
    1. 程序保存的是同一个索引tls_index = 3

  1. 但是使用该索引访问当前线程的 TLS 区域时,每个线程取到不同数据

Windows 原生 TLS API

  1. api

  1. 示例
    1. TlsAlloc() 分配的是全进程通用的“槽位编号”
    2. TlsSetValue() 设置的是当前线程在该槽位中的值
    3. TlsGetValue() 读取的是当前线程对应槽位中的值
    4. TlsFree() 释放槽位编号

现代 C++ 推荐 thread_local

  1. 示例

  1. 它比直接使用 TlsAlloc() 更自然:
    1. 类型安全
    2. 支持构造函数
    3. 支持析构函数
    4. 不需要手动分配 TLS 索引
    5. 不需要把指针转换成 void*
    6. 编译器和运行库负责线程生命周期集成
  2. 要记住的是:
    1. 不是“整个程序只有一个 context”,而是:
    2. 每个使用它的线程各自拥有一个 Context 对象
    3. 其初始化通常与线程首次按规则需要该对象相关,具体初始化细节还取决于对象种类和实现
    4. 线程结束时,正常情况下会执行相应的线程局部对象析构

TLS 不等于线程安全

  1. 此时安全
    1. temp_buffer 不需要因为其他线程而加锁
    2. shared_result 仍然存在数据竞争

  1. 此时不安全
    1. 虽然 ptr 本身是每线程独立的,但所有指针都指向同一个 global_object

  1. 因此判断线程安全时,要看最终指向和修改的数据是否共享,不能只看指针变量是不是 TLS

TLSFLS 的区别

  1. Windows 还有FLS(Fiber Local Storage)

  1. TLS 针对线程,FLS 可以针对 Fiber(纤程)
  2. 如果这些 Fiber 都在同一线程运行:
    1. TLS:它们看到的是同一个线程级值
    2. FLS:每个 Fiber 可以看到自己的值
  3. FLS 还支持清理回调

Windows纤程

概述

  1. 运行在线程内部、由应用程序自己切换的轻量级执行单元
  2. 线程由 Windows 内核调度;纤程主要由程序主动调度

纤程的基本 API

纤程拥有自己的什么

  1. 每个纤程都需要能够暂停并在将来恢复,所以它拥有独立的:
    1. 栈指针
    2. 指令执行位置
    3. 需要保存的寄存器上下文
    4. FLSFiber Local Storage)数据
    5. 与纤程切换相关的用户态状态

同一线程中的纤程共享:

  1. 所属线程
  2. 线程 ID
  3. TEB 中大量线程级状态
  4. TLS(Thread Local Storage)
  5. Win32 last-error 等线程状态
  6. 线程优先级
  7. CPU 时间片

线程切换和纤程切换

  1. 线程切换
    1. 线程切换由操作系统调度器决定
    2. 它通常会进入内核调度路径,而且可能涉及:
      调度器决策
      优先级
      时间片
      CPU 核心迁移
      缓存影响

  1. 纤程切换
    1. 纤程通常主动调用

纤程最大的危险:阻塞整个线程

  1. 假设 Fiber A 调用阻塞式 recv

  1. 如果数据迟迟不来:
    1. 因为 Windows 等待的是线程,不知道还想运行同一线程里的其他纤程

  1. 所以纤程调度器通常需要搭配:
    1. 非阻塞 I/O
    2. IOCP
    3. Overlapped I/O
    4. 定时器
    5. 等待完成后重新把纤程放进可运行队列

不能随便从纤程入口函数返回

  1. Windows Fiber 的入口函数不是普通线程函数
    1. 纤程过程返回可能导致承载线程退出,而不是自动回到创建它的纤程

纤程与 C++20协程的区别

对比项 Windows Fiber C++20 coroutine
抽象层次 Windows 平台 API C++ 语言机制
是否独立栈 是,有栈 通常无独立调用栈
暂停方式 SwitchToFiber co_awaitco_yield
切换单位 保存和恢复栈上下文 保存协程帧和状态
平台性 Windows 特有 跨平台语言标准
调度器 程序自行设计 程序或库自行设计
内存开销 通常更大 通常更小
普通深层函数能否直接让出 可以切换整个栈 通常需要协程调用链配合
  1. Fiber 是有栈协作式执行:
    1. func2() 切走时,整条调用栈都被保留

  1. C++20 协程通常是无栈协程
    1. 能够暂停的是协程函数本身,不能从任意普通深层函数中透明地暂停整个调用栈

示例

Linux线程

整体对比

内容 Windows Linux
内核调度对象 Thread Task,即 task_struct
用户态线程库 Win32、C++ runtime POSIX Threads,通常是 NPTL
创建线程 CreateThread pthread_create,底层使用 clone/clone3
线程执行现场 独立 独立
用户栈 独立 独立
内核栈 独立 独立
TLS thread_localTlsAlloc thread_local__threadpthread_key_create
TLS 快速定位 TEB、段寄存器机制 TCB、线程指针、段寄存器等
错误状态 GetLastError() errno
线程 ID Windows thread ID TID
进程 ID Process ID TGID,一般由 getpid() 返回
线程级信号屏蔽 Windows 模型不同 每线程信号掩码
Fiber 本地存储 FLS 没有统一等价的内核 Fiber API

Linux 内核怎样看待进程和线程

  1. Linux 内核为每个可调度任务维护一个类似下面的结构
    1. 无论它在用户看来是“进程”还是“线程”,内核调度器看到的主要都是 task

  1. 例如,一个进程有三个线程:
    1. 这三个任务各自可以被调度,但共享同一个进程级资源集合

  1. 创建线程时,本质上是在告诉内核:
    1. 创建一个新的可调度任务,但让它与当前任务共享地址空间、文件描述符、信号处理方式等资源
    2. 概念上可能使用以下共享标志:

CPU 执行上下文

  1. 每个线程拥有自己的(这与 Windows 基本一致):
    1. 指令指针
    2. 栈指针
    3. 通用寄存器
    4. 浮点和向量寄存器
    5. CPU 状态
    6. 调试相关状态

每个线程都有自己的线程栈

  1. 主线程的栈通常由程序加载和启动过程建立
    1. pthread_create() 创建的线程栈通常由 pthread 运行库负责分配和管理
  2. 可以设置线程栈大小
    1. 默认情况下,线程栈一般也会配置保护页,用于检测栈越界,但具体默认大小和布局受系统、架构及资源限制影响

内核栈

  1. 除了用户态栈,每个线程还有独立的内核栈
    1. 当线程执行系统调用:
    2. CPU 从用户态进入内核态,内核需要一块可靠的栈来执行系统调用代码

  1. 内核栈由内核管理,普通程序不能直接访问
  2. Windows 内核线程同样有相应的内核执行栈概念

调度状态

  1. 每个 Linux 线程是独立调度单位,因此拥有自己的:
    1. 运行、睡眠、停止等状态
    2. 调度策略
    3. 调度优先级
    4. CPU 亲和性
    5. CPU 使用时间
    6. 当前运行或最近运行的 CPU
    7. 调度统计信息
  2. 例如可以只设置某个线程的 CPU 亲和性
    1. 这表示该线程被限制到指定 CPU 集合,并不代表整个进程的所有线程都会自动使用完全相同的设置

Linux 的线程 ID

  1. Linux 中常见两类 ID

  1. 所以(同一进程的不同线程):
    1. getpid():通常相同
    2. gettid():不同
    3. pthread_self():不同,但类型和表示由 pthread 实现决定

Linux 线程本地存储 TLS

  1. Linux 中常见三种线程本地存储方式:
    1. C++ thread_local
    2. GCC/Clang __thread
    3. POSIX pthread_key_create
  2. C++ 首选thread_local

  1. GCC/Clang__thread
    1. 它提供线程局部存储,但能力比 C++ thread_local 更受限制,特别是涉及需要动态初始化和析构的 C++ 对象时

  1. POSIX pthread TLS
    1. 它在抽象上与 WindowsTlsAlloc() 很相似

  1. pthread_key_create(&g_key, destroy_value);
    1. 创建的是进程范围可用的 TLS
    2. 每个线程在这个键下拥有不同的值:

Linux TLS 的底层模型

  1. 先使用这个简化模型

  1. Linux 可执行文件和动态库可以包含 TLS 模板
    1. 创建线程时,运行库会为该线程准备相应的 TLS 区域
    2. x86-64 Linux 中,通常通过 FS 段基址快速定位当前线程相关结构
    3. 在其他架构上则可能使用专门的线程指针寄存器或架构约定

errno 为什么线程独立

  1. 每个线程拥有独立的 errno

Linux 每线程的信号状态

  1. Linux 信号机制中,既有进程级共享状态,也有线程级状态
  2. 每个线程拥有自己的信号屏蔽集合

  1. 信号处理方式通常是进程共享的
    1. 通常在同一线程组中共享
    2. 信号处理函数设置:进程范围共享
    3. 信号屏蔽集合:每线程独立

  1. 待处理信号既可能是线程级,也可能是进程级
    1. Linux 可以有:
    2. 定向发送给某个线程的信号
    3. 发送给整个进程的信号

文件描述符是共享还是独立

  1. 同一进程的 pthread 通常共享文件描述符表
    1. 如果线程 A 打开文件,把 fd 交给线程 B

地址空间、堆和全局变量共享

  1. 同一进程的线程共享:
    1. 虚拟地址空间
    2. 代码段
    3. 数据段
    4. 全局变量
    5. 普通静态变量
    6. 动态库映射
    7. mmap 映射
    8. 文件描述符表
    9. 当前工作目录等文件系统上下文
    10. 信号处理函数设置

pthread 取消状态也是线程级的

  1. POSIX 线程可以设置自己的取消状态:

  1. 其他线程可以请求取消:
    1. 但这不是粗暴地立即“杀掉线程”。默认的延迟取消通常要等目标线程运行到取消点

  1. 另外可以注册清理逻辑:

线程名称也是线程级的

  1. Linux 可以给线程设置名称

/proc 如何体现线程

  1. 假设进程 PID1000,内部有三个线程:

Linux 有没有 Windows TEB 的对应物

  1. 没有完全一一对应、公开稳定的单个结构
    1. 可以近似理解为几个部分共同承担

TLS 在线程池中的陷阱

  1. 这点在 LinuxWindows 完全一样

线程退出时会发生什么

  1. 正常线程退出时,用户态运行库通常需要处理:
    1. C++ thread_local 对象析构
    2. pthread TLS key 的析构回调
    3. pthread 清理处理程序
    4. 线程栈释放
    5. 线程描述结构清理
    6. pthread_join 相关的退出值和同步
    7. 内核任务退出

最重要的 Linux 线程模型

  1. Linux 内核把线程视为共享了资源的可调度 task
  2. 每个线程都有独立的执行现场、用户栈、内核栈、TLSTID和调度状态
  3. 同一进程中的线程通常共享地址空间、堆、全局变量和文件描述符表
  4. errno 是线程局部的,但失败后仍然必须立即保存;TLS 也只隔离变量本身,不会让它指向的共享对象自动线程安全

其他

POSIX 线程

  1. 创建过程

  1. 为什么叫“POSIX线程”而不直接叫“Linux线程”
    1. 因为 pthread API 不只存在于 Linux
    2. Linux
    3. FreeBSD
    4. macOS
    5. 其他 Unix-like 系统
    6. 它们都可以提供(但底层内核实现不一定相同)

  1. 总结
    1. POSIX线程是用户态标准和编程接口
    2. 通过 NPTL 实现后,每个 pthread 通常对应一个真正的 Linux 内核调度任务

NPTL

  1. Native POSIX Thread Library,原生 POSIX 线程库
  2. 是现代 Linuxglibc 实现 POSIX 线程接口的主要方案

声明:本文为原创文章,版权归所有,欢迎分享本文,转载请保留出处!

bingliaolong
Bingliaolong 关注:0    粉丝:0
Everything will be better.

发表评论

表情 格式 链接 私密 签到
扫一扫二维码分享