当前位置: 首页 > news >正文

多线程业务

没有操作系统,就无法实现多线程

严格意义上的多线程(抢占式、由独立调度器管理、具备完整同步机制)确实依赖操作系统。

多线程(Multithreading)的本质是CPU时间分片 + 执行上下文切换 + 资源调度,这三者都依赖操作系统提供的基础设施:

功能操作系统的作用
线程创建/销毁需要内核提供系统调用(如clone()CreateThread
调度算法OS 调度器决定哪个线程在何时运行(时间片轮转、优先级抢占等)
上下文切换保存/恢复寄存器、程序计数器、栈指针等状态
内存隔离线程栈空间的分配与管理
同步机制互斥锁、信号量等依赖内核原语实现

例外情况(裸机/嵌入式)

没有操作系统的裸机环境中,可以通过以下方式模拟"类多线程"行为:

  1. 协作式调度(Cooperative Multitasking)

    • 手动在代码中切换任务状态机

    • 没有真正的并行,只是逻辑上的并发

  2. 硬件中断 + 状态机

    • 利用定时器中断触发任务切换

    • 本质上是一个极简的"微内核"雏形

  3. 多核 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只有进程,没有线程概念
1990s+Solaris、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/C++pthread_create()/std::thread内核态POSIX线程和C++11线程都是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个,通常N=CPU核心数)

代表实现

  • Go: Goroutine + Go Scheduler + OS Threads

  • Java Virtual Threads: Virtual Thread → Carrier Thread → OS Thread

  • Rust Tokio: Async Task → Worker Threads → OS Threads

这种模型结合了用户态线程的轻量级和内核态线程的多核并行能力,是当前并发编程的主流方向。

竞态

多个线程完全可以在同一时间段内(甚至真正的同一时刻)执行同一个函数。但这正是多线程设计的正常行为,线程安全不依赖于"是否同时执行",而依赖于"如何访问数据"。

┌─────────────────────────────────────┐ │ 代码段(只读共享) │ │ ┌─────────────────────────┐ │ │ │ void worker(int x) { │ │ │ │ int a = x + 1; │ │ │ │ return a * 2; │ │ │ │ } │ │ │ └─────────────────────────┘ │ └─────────────────────────────────────┘ ▲ │ 所有线程共享这份机器码 ┌───────┴───────┐ ▼ ▼ ┌─────────┐ ┌─────────┐ │ 线程 A │ │ 线程 B │ │ 的执行上下文│ │ 的执行上下文│ ├─────────┤ ├─────────┤ │ PC=0x100│ │ PC=0x104│ ← 程序计数器(执行位置不同!) │ x=5 │ │ x=10 │ ← 参数(寄存器或栈中) │ a=6 │ │ a=11 │ ← 局部变量(各自栈上) │ RAX=12 │ │ RAX=22 │ ← 返回值寄存器 └─────────┘ └─────────┘ │ │ ▼ ▼ ┌─────────┐ ┌─────────┐ │ 线程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读取0,B读取0,A写回1,B写回1 → 结果应该是2,实际是1!(丢失更新)

2. 共享堆内存

int *shared_ptr; // 全局指针 void unsafe_heap() { if (shared_ptr != NULL) { // 检查和使用之间可能有其他线程修改! *shared_ptr = 100; // 可能访问已释放内存 } }

线程安全的本质

线程安全的关键不是"代码是否共享",而是"可变数据是否被并发访问"。函数代码天然是只读的,所以共享无妨;但数据一旦共享且可写,就必须同步。

组件是否共享是否安全原因
代码(函数体)共享✅ 安全只读,不可修改
程序计数器 PC独立✅ 安全每个线程有自己的执行位置
寄存器独立✅ 安全上下文切换时保存/恢复
栈(局部变量)独立✅ 安全每个线程独立栈空间
堆(动态内存)看情况⚠️ 看情况取决于指针是否被共享
全局/静态变量共享❌ 危险需要互斥锁等同步机制
http://www.cnnetsun.cn/news/1784415.html

相关文章:

  • 基于LM2596的Buck电路设计
  • GetQzonehistory:3步轻松备份QQ空间全部历史说说,守护你的青春记忆
  • 3个实用技巧让UE4SS成为你的游戏Mod开发利器
  • 《Spring AI 实战系列 入门篇》第 2 篇
  • 一文看懂:基于深度学习的 ISAC 波形与IRS相位联合优化Python开源代码
  • wxhelper:企业级微信自动化开发框架的跨版本适配实践指南
  • 复习Java
  • Resource Override:前端开发中的资源重定向解决方案
  • windows环境oracle 11.2.0.1版本数据库启动报错ORA-01589问题的处理
  • 【Java戒烟网站】(免费领源码+演示录像)|可做计算机毕设Java、Python、PHP、小程序APP、C#、爬虫大数据、单片机、文案
  • 终极指南:3步安装Koikatu HF Patch,解锁完整游戏体验
  • 【PHP微服务升级必读】:Swoole 5.0全栈适配指南(含兼容性矩阵与3大避坑清单)
  • 收藏 | 提示词、提示词工程、上下文工程,小白程序员必看!大模型学习入门
  • 学Simulink——基于SVPWM的过调制(Overmodulation)策略扩展电压输出能力
  • 【ubuntu20.04安装nvidia显卡驱动及pytorch】
  • 2026 答辩季实测:PaperXie AI PPT 生成器,把毕业论文答辩 PPT 从 “熬夜噩梦” 变成 “一键惊艳”
  • AICoverGen语音转换全攻略:从基础搭建到创意实践
  • 建筑行业企业大数据可视化大屏系统源码(Vue3+DataV架构)
  • 7个实用技巧:Ryujinx模拟器从入门到精通
  • 一图看懂 Windows 目录结构:C 盘这些文件夹到底是干什么的?
  • 3个核心功能让ASMR爱好者一键构建个人音频库:asmr-downloader技术解析
  • 互联网行业自动化平台选型,运营全流程提效指南:2026企业级智能体架构与实战全解析
  • 科技中介如何借助数字化工具提升服务的专业性与覆盖面?
  • Pretext:值得关注的文本排版引擎劝
  • 如何快速清理Windows 11系统垃圾:免费工具Win11Debloat完整使用指南
  • 4个效率倍增技巧:D3KeyHelper让暗黑3操作自动化更精准
  • 微信对接OpenClaw的常见问题和解决方案盘
  • iperf3实战指南:解决网络性能瓶颈的测试方法论
  • Sketch Measure:设计协作效率引擎的5大核心场景与实战指南
  • 企业级管理系统的架构设计与实战指南