操作系统学习笔记(二) 进程、线程与协程

发布时间:2026/9/26 3:06:07
操作系统学习笔记(二) 进程、线程与协程 操作系统学习笔记(二) 进程、线程与协程一、三种抽象处在不同层级进程、线程、协程不是互相替代的三个方案——一个服务可以启动多个进程每个进程拥有若干线程其中某个线程再运行事件循环承载成千上万个协程。它们可以组成一棵执行树操作系统 └─ 进程资源与隔离容器 ├─ 线程CPU 调度单元 │ ├─ 协程 A等待网络 │ ├─ 协程 B等待数据库 │ └─ 协程 C处理定时任务 └─ 线程处理同步阻塞调用这三层关注的问题分别是层级解决了什么问题进程如何划分资源、权限与故障边界线程如何在一个进程内建立多个可被操作系统独立调度的执行流使多个任务能够并发推进协程如何让一个线程承载多个可暂停的逻辑任务并在它们之间协作切换二、进程资源和故障的边界进程是程序的一次运行实例。进程拥有独立的虚拟地址空间以及文件描述符、环境变量、安全身份等运行资源。两个进程默认不能直接读写彼此的内存。它们需要通过管道、Socket、消息队列或共享内存通信。这个限制增加了通信成本却换来了清晰的隔离一个进程内存损坏或崩溃通常不会直接破坏另一个进程的调用栈和堆。进程重要价值在与它是可靠的终止边界。操作系统可以终止一个具体进程并回收它占用的大部分资源(强制终止线程通常不安全,可能破坏仍被其他线程使用的共享状态)。因此不可信、可能死锁或者必须支持强制回收的工作适合放进独立进程中而不是直接塞进主服务线程。进程的代价也很明确。创建和切换进程比协程更重进程间数据需要序列化、复制或同步。它适合换取隔离不适合替代所有轻量任务。三、线程进程里的执行单元现代操作系统通常实际调度线程。进程提供资源容器线程带着寄存器、栈和执行位置去使用 CPU。从 Python 代码创建一个线程Python 标准库通过threading.Thread创建线程importthreadingimporttimedefdownload(filename):print(f{threading.current_thread().name}开始下载{filename})time.sleep(2)print(f{filename}下载完成)workerthreading.Thread(targetdownload,args(report.pdf,),namedownload-worker,)print(worker.is_alive())# False此时还没有启动新线程worker.start()print(worker.is_alive())# 通常为 True新线程正在运行print(主线程继续处理其他事情)worker.join()# 等待 worker 执行结束print(所有工作完成)这里有三个关键动作Thread(...)创建一个 Python 层的线程对象并保存目标函数、参数和名称此时还没有创建操作系统线程start()真正申请并启动一个新线程新线程随后执行run()再由默认的run()调用targetjoin()让当前线程等待目标线程结束它不会启动或终止目标线程。如果直接调用worker.run()download()会像普通函数一样在当前线程中执行不会产生并发。一个Thread对象也只能成功调用一次start()。进程启动时已经有线程吗有。一个正在运行的普通进程至少需要一个线程来执行指令。操作系统创建 Python 进程时也会创建它的初始线程Python 把它称为主线程即MainThread。模块顶层代码、程序入口和没有显式创建线程的普通 Python 代码默认都由这个线程执行。importthreadingprint(threading.current_thread().name)# MainThreadprint(threading.active_count())# 简单程序中通常是 1调用start()后发生了什么以常见的 CPython 实现为例新线程的启动过程可以概括为主线程创建 Thread 对象 ↓ 主线程调用 start() ↓ Python 运行时请求操作系统创建原生线程 ↓ 操作系统分配线程 ID、调度状态、寄存器现场和专用栈 ↓ 新线程进入 CPython 的启动代码建立自己的线程状态 ↓ 新线程执行 Thread.run()进而调用 target ↓ target 返回线程退出join() 的等待者被唤醒新线程不是把主线程完整复制一份。操作系统不会复制整个进程地址空间也不会复制主线程当前的调用栈。新线程直接加入同一个进程因此可以访问同一份代码、全局变量、堆对象、文件描述符和网络连接与此同时它获得自己的寄存器状态、指令位置、线程局部存储和栈区域。在常见的 CPython 构建中新线程执行 Python 字节码前还需要获得 GIL。线程遇到阻塞 I/O 或运行时切换时其他线程才有机会继续执行 Python 代码。GIL 不等于“只有一个线程”它限制的是多个线程同时执行 Python 字节码。新线程的栈和局部存储从哪里来简短答案是操作系统与线程运行库负责分配但这些内存仍然位于当前进程的虚拟地址空间中。线程不会因此获得一套独立于进程之外的地址空间。进程虚拟地址空间 ├─ 代码区 ├─ 全局数据 ├─ 共享堆 ├─ 主线程的栈 ├─ 线程 A 的栈 ├─ 线程 B 的栈 ├─ 线程 A 的 TLS(线程局部存储) └─ 线程 B 的 TLS创建新线程时线程运行库会请求操作系统在当前进程的虚拟地址空间中为它预留栈区域。操作系统还会在内核中建立线程 ID、调度状态、寄存器现场和优先级等管理信息。两类资源处在不同位置位置典型内容进程虚拟地址空间线程栈、线程局部存储、运行时控制结构操作系统内核线程 ID、调度状态、保存的寄存器、优先级线程栈通常先预留一段虚拟地址真正访问相应页面时才逐步占用物理内存。栈边界附近一般还会设置保护页线程越界访问时便会触发栈溢出错误。“线程专用栈”描述的是默认归属和栈指针关系并不表示存在内存隔离。如果线程 A 得到了线程 B 栈中某个对象的地址它在底层仍可能访问这块内存因为两个线程共享同一个进程地址空间。线程局部存储 TLS 也是类似的。程序看起来访问同一个变量名运行时会根据当前线程找到各自的数据副本importthreading request_contextthreading.local()request_context.user_id123线程 A 和线程 B 可以拥有不同的user_id但这些数据仍然存放在进程地址空间中只是由运行时按线程身份进行索引。还要区分线程栈与 Python 局部变量。Python 对象大多分配在进程堆中函数执行现场保存的是对这些对象的引用。不能简单地认为一个 Python 函数里的所有局部数据都直接存放在原生线程栈上。为什么线程不能共用一个栈调用函数时需要记录参数、局部变量、返回地址和调用链。例如两个线程同时处理订单defcharge(order_id):amount100pay(order_id,amount)它们的调用现场不同线程 A 的栈 线程 B 的栈 ┌────────────────┐ ┌────────────────┐ │ pay(order_id1)│ │ pay(order_id2)│ ├────────────────┤ ├────────────────┤ │ charge │ │ charge │ │ amount 100 │ │ amount 100 │ │ order_id 1 │ │ order_id 2 │ └────────────────┘ └────────────────┘如果两个线程使用同一个栈线程 B 压入的参数和返回地址可能覆盖线程 A 的现场函数将无法正确恢复。CPU 使用栈指针寄存器记录当前栈顶。操作系统切换线程时会保存旧线程的寄存器和栈指针再加载新线程的状态运行线程 ASP → A 的栈 运行线程 BSP → B 的栈 切回线程 ASP → A 原来的栈位置抢占式调度线程由操作系统抢占式调度。一个线程即使不主动让出 CPU时间片耗尽后也可能被暂停操作系统随后运行另一个线程。这种模型不依赖任务主动合作却带来了共享内存竞争。多个线程可能同时修改同一个对象因此需要锁、条件变量和信号量并谨慎处理竞态、死锁与内存可见性。在 CPython 中GIL 会限制多个线程同时执行 Python 字节码。Python 线程仍适合 I/O 和同步阻塞库纯 Python 的 CPU 密集计算通常更适合多进程。四、协程一个线程里的协作式调度协程是可以暂停和恢复的函数。Python 使用async def定义协程在await处保存执行现场并主动让出当前线程。asyncdefnoodles():print(面条下锅)awaitasyncio.sleep(3)print(面条好了)asyncdefegg():print(鸡蛋下锅)awaitasyncio.sleep(1)print(鸡蛋好了)awaitasyncio.gather(noodles(),egg())面条等待三秒时线程不必站在原地。事件循环可以运行鸡蛋协程。鸡蛋的定时器到期后它进入就绪队列等待事件循环重新调度。同一个线程在同一时刻只能执行一个协程。协程带来的并发来自交替推进而不是同时占用一个 CPU 核心。ACM 比赛类比可以把协程调度想成几个 ACM 队员共享一台电脑一台电脑 一个事件循环线程 队员 协程 在纸上推理 网络、数据库或远端服务在外部处理 上机写代码 获得线程执行权 提交后下机 在 await 处主动让出线程 保存代码和思路 保留局部变量与执行位置 队长排上机顺序 事件循环调度队员 A 提交代码等待判题后下机队员 B 随即使用电脑。A 的判题结果返回时它只会进入上机队列不会把正在运行的 B 强行赶走。这就是协作式调度。协程需要在合适的位置主动await事件循环才有机会运行其他任务。不合作的协程会发生什么asyncdeftroublemaker():whileTrue:calculate()这个协程始终不执行await便会独占事件循环线程。其他协程、网络回调、定时器和 watchdog 都无法运行。因此长时间同步阻塞或 CPU 计算不能直接放进事件循环。普通阻塞函数可以交给线程池resultawaitasyncio.to_thread(blocking_function)需要多核并行、故障隔离或强制终止的任务则应交给独立进程。五、并发不等于并行并发描述多个任务在一段时间内共同推进并行描述多个任务在同一时刻使用不同 CPU 核心。单线程协程能够并发处理大量 I/O却不能让两个 CPU 密集任务在同一个核心上同时执行。它提升的通常是吞吐量和资源利用率不会缩短远端模型本身的推理时间。一次模型调用可能耗时 30 秒但 Worker 线程实际只做两小段工作发送请求和处理响应。中间的推理发生在模型服务器的 CPU/GPU 上。协程在这段时间让出线程Worker 便能继续处理续约、监控和其他请求。按照工作性质选择执行模型工作类型合适的方式大量网络、数据库和定时等待协程只能同步调用的阻塞库线程池CPU 密集并且需要多核并行多进程不可信、可能卡死、需要强制终止子进程、Sandbox 或 Pod六、进程、线程和协程的取舍维度进程线程协程调度者操作系统操作系统事件循环/运行时调度方式抢占式抢占式通常是协作式内存关系独立地址空间共享进程地址空间共享所在线程和进程资源执行现场至少一个线程独立栈和寄存器coroutine/frame 对象创建和切换成本高中等低多核并行可以可以CPython 受 GIL 影响单线程内不可以故障隔离强弱弱强制终止可以终止进程通常不能安全强杀取消需要代码配合典型用途隔离、并行、不可信任务阻塞调用、共享内存并发大量 I/O 并发实际系统很少只选一种。更常见的组合是进程提供隔离线程承载运行时协程编排 I/O。Python相关QAQ为什么说使用Python批量处理计算密集时任务时最好使用多进程而不是多线程核心原因是CPython 的 GIL全局解释器锁——在默认的 CPython 中同一个 Python 进程内同一时刻只有一个线程能执行 Python 字节码。如果处理I/O密集型任务那么在等待IO的过程中线程会释放GIL和CPU核心每个线程占用GIL的时间很短线程虽然不能并行但是可以并发执行效率瓶颈在外部系统整体效率依旧很高如果是计算密集型任务效率瓶颈在于线程内部的计算计算过程中线程必须占据GIL和CPU核心其他线程此时只能挂起整体其实更加偏向于单线程的模型因此不适合Python不适合使用多线程来批量处理计算密集型的任务Q什么是GIL既然GIL引入了进程内线程不可并行的问题为什么还要用taGIL全称Global Interpreter Lock全局解释器锁是CPython 解释器里的一个互斥锁。同一个进程内的线程是会共享一部分空间的CPython为了解决其解释器内部的变量并发修改问题引入了GIL这样的大颗粒度的锁好处是实现简单。历史的局限性——早期大部分机器都是单核的Q什么是Cpython为什么要有这个东西CPython是Python的默认解释器其主要做两件事把Python语言编译为字节码用虚拟机解释执行字节码编译和解释有什么区别编译器和解释器最终都是让程序变成 CPU 能执行的机器指令但它们在中间可能生成字节码、IR 等非机器语言形式由运行时系统代为执行。这里的“计算机能执行的形式”不一定是机器语言但是最终一定是机器语言。对于CPU而言其能理解的确实只有机器语言但是现代计算器不止有CPU还可以通过其他手段——比如OS、虚拟机、运行时环境等从而让计算机能理解更加高级的语言C 语言hello.c - 编译器 - 机器码 - CPU 直接执行这里可以说高级代码直接变成了机器语言。Pythonhello.py - CPython 编译成字节码 - CPython 虚拟机解释字节码 - 执行对应机器指令Python 源码并没有直接变成机器语言。是 CPython 解释器这个机器码程序根据字节码去执行相应的机器指令。JavaHello.java - 字节码 - JVM 解释/JIT - 机器码 - CPU 执行字节码也不是 CPU 直接执行的但 JVM 能执行它QPython为什么不需要将字节码编译为机器码类似于命令模型的原理——命令模式的核心在与调用者不需要了解每个命令后的具体原理与过程只需要负责下发命令具体操作交给接收者这里字节码就像是命令而机器码就是命令背后的具体操作解释器只需要读到机器码就会执行对与的C语言函数这些函数以及被提前编译为了机器码因此字节码并不需要全部编译为机器码再整个运行这个转化的过程在解释的过程中间接完成Q把字节码整个编译为机器码不是更好吗这样一次编译就可以到处运行省去了每次解释的开销因为Python不确定的东西太多Python本身是动态类型的语言变量的类型要等到运行时才知道字节码可以跨平台运行(通过不同平台的解释器解释成不同平台的机器码)但是机器码本身不行

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询