SYSTEMS / 001//4 分钟阅读
进程与线程:程序是怎样同时运行的
从资源隔离和执行调度两个角度,理解进程、线程、并发与并行。
程序是磁盘上的指令与数据;进程是程序运行起来后的一个实例。打开两个相同的命令行程序,可以得到两个独立的进程,而不只是同一段程序「多执行了一次」。
资源与执行是两个视角
进程通常拥有独立的虚拟地址空间,以及文件描述符等资源。隔离意味着一个进程不能随意访问另一个进程的普通内存,但这不代表进程之间无法通过共享内存等机制通信。
线程是进程内部的一条执行路径。同一进程中的线程共享地址空间与许多资源,同时各自保有寄存器状态、栈等执行上下文。
| 观察角度 | 进程 | 同一进程内的线程 |
|---|---|---|
| 地址空间 | 通常相互隔离 | 共享 |
| 通信方式 | 管道、套接字、共享内存等 | 可以直接读写共享数据 |
| 出错影响 | 通常限于当前进程 | 可能影响整个进程 |
| 数据协调 | 需要明确通信机制 | 仍然需要同步 |
并发不一定并行
一个厨师交替处理多道菜,是并发;多个厨师在同一时刻各自做菜,是并行。前者强调任务在一段时间内共同推进,后者强调同一时刻实际执行多个任务。
即使只有一个 CPU 核心,操作系统也能通过调度让多个任务交替运行。切换发生得足够快,就会给人「同时运行」的感觉。多核提供了并行执行的条件,但程序能否有效利用,还取决于任务如何拆分。
共享状态为什么容易出错
假设两个线程都执行 count = count + 1。这句话可以拆成读取、计算和写回:
线程 A:读取 count = 0
线程 B:读取 count = 0
线程 A:写入 count = 1
线程 B:写入 count = 1
执行了两次加一,结果却可能只增加一次。这是理解竞态条件的直观例子。对于某些编程语言,未同步的数据竞争还可能直接构成未定义行为,不能只把它当作「偶尔少加一次」。
互斥锁、原子操作和消息传递可以帮助建立正确的协调关系。选择哪个机制,要从数据的所有权与访问顺序出发。
在线程数之前,先问任务是什么
- 计算密集任务受 CPU 和算法效率影响,线程过多可能增加切换成本。
- 等待网络或磁盘的任务,可以用线程池或异步 I/O 减少等待带来的空闲。
- 共享数据越多,协调成本通常越高。先减少共享,再讨论锁的粒度。
# 查看 Linux 进程与线程
ps -eLf
# 查看一个指定进程的线程
ps -T -p 1234
理解进程与线程,关键是同时记住两件事:谁拥有资源,谁推进执行。把这两个问题分开,很多并发设计就会清晰起来。
继续阅读
EOF / 感谢阅读继续探索 ↗