并行/并发
cpu是如何处理这么多进程的?
每个进程内部都有很多线程,cpu的核心会轮流执行每个线程一段时间,然后中断去执行别的线程,因为这个时间极短,所以看起来像是在一直运行,这就是并发
并发:是逻辑上的“同时处理”,本质是轮流切换(看起来像同时做)。
并行:是物理上的“同时执行”,本质是一起开工(真正的同时做)。
并发是关于如何写代码:用多线程、协程、异步回调去处理多个独立的业务流。
并行是关于如何运行代码:你的程序是否真的在利用多核 CPU 同时运算。
线程模型
大部分编程语言里的线程都不等于操作系统的线程
内核线程 (Kernel Thread):这是操作系统(如 Windows、Linux)真正管理、调度的执行单元。创建和切换它开销很大,需要陷入内核态(系统调用)。
用户态线程 (User-level Thread):这是编程语言或运行时(Runtime)在用户空间自己管理和调度的“虚拟线程”。我们平时在代码里 new Thread() 或 go func() 创建的大多属于这一类。
- 1:1 模型(直接映射)
语言创建的每个线程,都直接对应一个操作系统内核线程。
代表:Java (传统线程)、Rust (标准库线程)、C++ (std::thread)。
特点:实现简单,能充分利用多核 CPU。但线程数量受限(通常只能创建几千个),创建和切换成本较高。
- N:1 模型(多对一)
语言在用户空间管理多个“协程”或“轻量级线程”,但底层只用一个内核线程来跑。
代表:早期的 Python (Gevent)、Node.js (异步回调)。
特点:切换极快,能轻松创建成千上万个任务。但无法利用多核 CPU 并行计算(只能并发),一旦这个内核线程阻塞,整个程序都会卡住。
- M:N 模型(多对多,最灵活)
语言运行时会创建 M 个用户线程,然后动态地绑定到 N 个内核线程上(N < M)。
代表:Go (Goroutine)、Erlang (进程)、Java (虚拟线程,即 Project Loom)。
特点:可以创建海量(百万级)的“线程”,切换开销极小,同时又能充分利用多核 CPU。这是目前公认最先进的线程模型。
既然操作系统有现成的线程,为什么语言还要自己搞一套呢?核心原因有两点:
切换成本的天壤之别:
操作系统切换线程(上下文切换)需要保存大量寄存器、切换内存映射,耗时在微秒级。
编程语言切换自己的用户态线程,通常只需要保存几个程序计数器和栈指针,耗时在纳秒级,效率高几个数量级。
海量并发能力的差异:
一个操作系统线程默认占用约 8 MB 的栈内存,如果创建 1 万个线程,仅内存就需要 80 GB,根本无法实现。
像 Go 的 Goroutine,初始栈只有 2 KB,并且会动态扩缩容。创建 10 万个 Goroutine 对现代服务器来说轻轻松松。
https://docs.gitlab.com/user/project/merge_requests/reviews/stacked_merge_requests/