多线程业务
没有操作系统就无法实现多线程严格意义上的多线程抢占式、由独立调度器管理、具备完整同步机制确实依赖操作系统。多线程Multithreading的本质是CPU时间分片 执行上下文切换 资源调度这三者都依赖操作系统提供的基础设施功能操作系统的作用线程创建/销毁需要内核提供系统调用如clone()、CreateThread调度算法OS 调度器决定哪个线程在何时运行时间片轮转、优先级抢占等上下文切换保存/恢复寄存器、程序计数器、栈指针等状态内存隔离线程栈空间的分配与管理同步机制互斥锁、信号量等依赖内核原语实现例外情况裸机/嵌入式在没有操作系统的裸机环境中可以通过以下方式模拟类多线程行为协作式调度Cooperative Multitasking手动在代码中切换任务状态机没有真正的并行只是逻辑上的并发硬件中断 状态机利用定时器中断触发任务切换本质上是一个极简的微内核雏形多核 MCU 的裸机并行如 ARM Cortex-M 的 SMP 模式但仍需要极简的调度逻辑严格来说已接近微内核CPU核心与线程单个 CPU 核心 一个指令指针 (PC) 同一时刻只能推进一个线程的执行流。进程不是执行的单位线程才是 CPU 调度的基本单位。一个CPU核心在任意时刻执行的是某个线程的指令这个线程必然属于某个进程线程不能脱离进程存在但核心不关心这是哪个进程的线程——调度器只关心线程的优先级、状态等时间线 → ├─ 核心执行 Thread-A属于进程 P1 ├─ 上下文切换 ─────────────────────────┐ ├─ 核心执行 Thread-B属于进程 P2←───┘ 不同进程无缝切换 ├─ 上下文切换 ─────────────────────────┐ ├─ 核心执行 Thread-C属于进程 P1←───┘ 又切回 P1进程切换 vs 线程切换的区别同进程线程切换只需换栈、寄存器页表不变轻量跨进程线程切换还要切换地址空间页表、刷新 TLB稍重但无论哪种同一时刻只有一个线程在跑。线程切换线程切换确实极其频繁频繁到超出常人想象。但这正是操作系统设计的精妙之处——它让这种高频切换便宜到几乎无感知。你的电脑不是同时跑 3000 个线程而是以每秒数万次的频率在 几个核心上快速接力跑其中最需要 CPU 的那几十个线程。场景时间尺度类比现代 CPU 主频~3-5 GHz每秒 30-50 亿个时钟周期一次线程上下文切换~1000-2000 个时钟周期约0.3-0.6 微秒百万分之一秒Linux 默认时间片100 毫秒CFS 调度但线程可能几毫秒就主动让出实际切换频率每秒数千到数万次甚至更高1. 绝大多数线程在睡觉系统状态示例你的电脑此刻 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 总线程数: ~3000个 ├─ 运行中 (Running): 4个 ← 等于你的 4 核 CPU ├─ 可运行 (Runnable): 12个 ← 等着被调度 ├─ 睡眠等待 (Sleeping): 2980个 ← 等I/O、等定时器、等用户点击... └─ 僵尸/停止: 4个 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━真相95% 的线程处于阻塞/睡眠状态根本不消耗 CPU。它们可能在等硬盘读完文件几十毫秒等网络响应几百毫秒等用户按键盘可能几分钟等定时器到期如每秒刷新一次的时钟2. 切换成本被设计得极低现代 CPU 和 OS 的优化硬件支持专用寄存器快速保存/恢复上下文TLB 缓存减少地址切换开销内核优化Linux 的vDSO让用户态快速获取时间减少进内核次数避免不必要切换线程连续运行几毫秒才切不是微秒级乱切3. 人类感知 vs 机器时间人类感知CPU 视角同时打开 10 个网页每个网页的渲染线程分片执行每片几毫秒音乐流畅播放音频线程每 5-10 毫秒被唤醒填充一次缓冲区鼠标立刻响应中断瞬间抢占延迟 1 毫秒三态模型进程/线程现代操作系统中CPU 调度的基本单位是线程不是进程。但三态模型确实常被用来讲进程如网上可以搜到的很多资料和教材这是历史遗留 概念简化造成的混淆。年代代表系统特点1960s-1980sUnix 早期、DOS只有进程没有线程概念1990sSolaris、Windows NT、Linux 2.6引入内核级线程在早期系统中进程 执行单位 资源单位调度器选哪个进程运行现代 OS 的演变早期模型 现代模型 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 进程 执行流 进程 资源容器地址空间、文件描述符等 ↓ ↓ 调度器调度进程 调度器调度线程 ↓ ↓ 进程切换 换执行换资源 线程切换 换执行同进程内资源不变 进程切换 换线程 换资源更重三态模型到底属于谁三态模型描述的是执行实体的生命周期这个实体可以是语境实际指代正确性早期教材/简单OS进程因为当时没有线程✅ 历史正确现代OS内核实现线程内核调度实体✅ 技术正确用户态库线程如Python threading用户态线程由运行时管理⚠️ 模拟的伪并行内核层真正发生 CPU 调度内核看到的 ┌─────────────────────────────────────────┐ │ 进程P1 进程P2 进程P3 │ │ ├─ 线程T1 ├─ 线程T3 ├─ 线程T5 │ │ └─ 线程T2 └─ 线程T4 ... │ │ │ │ 调度器从 {T1, T2, T3, T4, T5...} 中选择 │ │ ▲ 无视进程边界只看线程优先级、状态 │ └─────────────────────────────────────────┘内核调度的是线程Linux 中叫task_struct代表一个可调度的任务。Linux 的哲学线程只是共享地址空间的进程。// Linux 内核代码简化 struct task_struct { // 这个结构体既代表进程也代表线程 // 区别只在 flags 和 mm内存描述符的共享方式 volatile long state; // 状态TASK_RUNNING, TASK_INTERRUPTIBLE... int prio, static_prio; // 动态/静态优先级 struct sched_entity se; // CFS 调度实体 struct mm_struct *mm; // 地址空间线程共享进程独占 pid_t pid; // 线程ID pid_t tgid; // 进程组ID线程组leader的PID };fork() 创建新进程新地址空间clone(CLONE_VM) 创建线程共享地址空间调度器一视同仁都按task_struct调度。内核态/用户态线程内核态线程Kernel-Level Thread / OS Thread本质由操作系统内核直接管理和调度的线程每个线程对应一个内核调度实体。特点线程的创建、销毁、调度都由操作系统内核完成线程切换需要陷入内核态涉及上下文切换开销较大可以利用多核CPU的并行能力真正的并行执行线程阻塞如I/O操作不会阻塞整个进程资源占用相对较大每个线程需要独立的内核栈等用户态线程User-Level Thread / Green Thread / 协程本质完全在用户空间实现的线程机制操作系统看不见这些线程内核只感知到一个普通的进程或单个线程。特点线程管理创建、调度、切换完全由用户空间的运行时库/虚拟机完成切换不需要进入内核态开销极小类似函数调用无法直接利用多核并行除非绑定到多个内核线程上即M:N模型一个用户态线程阻塞会导致整个内核线程阻塞早期实现的问题现代实现已改善可以创建海量线程内存占用极小通常几KB需要运行时支持协作式调度或抢占式调度维度内核态线程用户态线程管理者操作系统内核用户空间运行时/虚拟机切换成本高需陷入内核保存大量寄存器状态低纯用户空间操作创建数量有限通常几千个受内存限制海量可创建数百万个多核利用原生支持真正并行需映射到多个内核线程才能并行阻塞影响单个线程阻塞不影响其他线程早期模型会阻塞整个进程现代实现已优化调度策略内核决定抢占式运行时决定协作式或轻量级抢占主流语言的多线程实现语言语法/API线程类型说明C/Cpthread_create()/std::thread内核态POSIX线程和C11线程都是1:1映射到OS线程Javanew Thread()/ExecutorService内核态传统Java线程是内核线程1:1模型JavaVirtualThread(Java 21)用户态Project Loom引入的虚拟线程M:N模型Gogo func()用户态Goroutine是经典用户态线程Go运行时调度M:N模型Pythonthreading.Thread内核态受GIL限制同一时刻只有一个线程执行Python字节码Pythonasyncio/asyncawait用户态协程实现单线程事件循环非抢占式Ruststd::thread::spawn内核态原生OS线程Rusttokio::spawn(async)用户态基于work-stealing的异步运行时用户态调度JavaScriptnew Worker()内核态Web Worker是独立OS线程浏览器/Node.js环境JavaScriptasync/await/ Promise用户态事件循环中的协程单线程非阻塞C#new Thread()/Task.Run内核态.NET线程池线程映射到OS线程C#async/await混合Task异步模型可能运行在池化线程或当前上下文Kotlinlaunch/async(协程)用户态Kotlin Coroutines编译为状态机可挂起恢复Erlang/Elixirspawn用户态BEAM虚拟机的轻量级进程非OS进程M:N模型现代趋势混合模型M:N现代高性能运行时普遍采用M:N 模型混合线程模型用户态线程M个 → 调度器 → 内核线程N个通常NCPU核心数代表实现Go: Goroutine Go Scheduler OS ThreadsJava Virtual Threads: Virtual Thread → Carrier Thread → OS ThreadRust Tokio: Async Task → Worker Threads → OS Threads这种模型结合了用户态线程的轻量级和内核态线程的多核并行能力是当前并发编程的主流方向。竞态多个线程完全可以在同一时间段内甚至真正的同一时刻执行同一个函数。但这正是多线程设计的正常行为线程安全不依赖于是否同时执行而依赖于如何访问数据。┌─────────────────────────────────────┐ │ 代码段只读共享 │ │ ┌─────────────────────────┐ │ │ │ void worker(int x) { │ │ │ │ int a x 1; │ │ │ │ return a * 2; │ │ │ │ } │ │ │ └─────────────────────────┘ │ └─────────────────────────────────────┘ ▲ │ 所有线程共享这份机器码 ┌───────┴───────┐ ▼ ▼ ┌─────────┐ ┌─────────┐ │ 线程 A │ │ 线程 B │ │ 的执行上下文│ │ 的执行上下文│ ├─────────┤ ├─────────┤ │ PC0x100│ │ PC0x104│ ← 程序计数器执行位置不同 │ x5 │ │ x10 │ ← 参数寄存器或栈中 │ a6 │ │ a11 │ ← 局部变量各自栈上 │ RAX12 │ │ RAX22 │ ← 返回值寄存器 └─────────┘ └─────────┘ │ │ ▼ ▼ ┌─────────┐ ┌─────────┐ │ 线程A的栈│ │ 线程B的栈│ │ [独立] │ │ [独立] │ └─────────┘ └─────────┘单核 CPU时间片轮转时间轴 ──────────────────────────────────────────────► CPU 核心1: [线程A:worker()] [线程B:worker()] [线程A:worker()] ... └───── 10ms ─────┘└───── 10ms ─────┘ 实际上: 快速切换看起来同时实际是交替执行多核 CPU真正并行时间轴 ──────────────────────────────────────────────► CPU 核心1: [线程A:worker()]──────────────────────────► └──────────────────────────────────────────┘ CPU 核心2: [线程B:worker()]──────────────────────────► └──────────────────────────────────────────┘ 同时刻: 两个核心真的在执行同一份代码的不同位置区分代码共享 vs 数据共享void safe_function(int x) { // x 在寄存器或栈上每个线程独立 int local x 1; // local 在栈上每个线程独立 int *ptr malloc(4); // ptr 在栈上独立指向堆看情况 *ptr local; // 如果 ptr 指向线程私有内存安全 // 线程安全的原因所有状态都是独立的 } void unsafe_function(int x) { static int shared 0; // ❌ 静态区所有线程共享 shared x; // ❌ 竞态条件 global_var; // ❌ 全局变量共享 }线程不安全的情况1. 静态/全局变量数据共享static int counter 0; // 在数据段共享 void unsafe_increment() { counter; // 读-改-写三步非原子操作 // 实际汇编 // MOV EAX, [counter] ; 读取可能读到旧值 // INC EAX ; 修改 // MOV [counter], EAX ; 写回可能覆盖其他线程的修改 } // 线程A和B同时执行 // A读取0B读取0A写回1B写回1 → 结果应该是2实际是1丢失更新2. 共享堆内存int *shared_ptr; // 全局指针 void unsafe_heap() { if (shared_ptr ! NULL) { // 检查和使用之间可能有其他线程修改 *shared_ptr 100; // 可能访问已释放内存 } }线程安全的本质线程安全的关键不是代码是否共享而是可变数据是否被并发访问。函数代码天然是只读的所以共享无妨但数据一旦共享且可写就必须同步。组件是否共享是否安全原因代码函数体共享✅ 安全只读不可修改程序计数器 PC独立✅ 安全每个线程有自己的执行位置寄存器独立✅ 安全上下文切换时保存/恢复栈局部变量独立✅ 安全每个线程独立栈空间堆动态内存看情况⚠️ 看情况取决于指针是否被共享全局/静态变量共享❌ 危险需要互斥锁等同步机制