#29 计算机基础 简单

进程和线程的区别?

进程线程地址空间并发GIL

🎯 考察意图

操作系统基础题,数据/后端岗常用来铺垫「为什么 Python 算密集要多进程、Spark 一个 executor 里又能跑多个 task」。考察候选人能否从资源分配、地址空间、通信与崩溃隔离讲清楚,而不是只背「进程重、线程轻」。

📐 回答框架

  1. 先给定义:进程是资源分配单位,线程是 CPU 调度单位。
  2. 按对比项说:地址空间、资源、通信、隔离、创建/切换成本。
  3. 落到工程选型:Python GIL、Spark executor/task、IO 密集 vs CPU 密集。
  4. 收口:怎么选,以及别把并发和并行混为一谈。

💬 参考答案(口述版)

一句话:进程是操作系统分配资源(地址空间、文件句柄、一套独立内存)的单位;线程是被调度上 CPU 跑的单位,同一进程里的线程共享这份资源。

对比着说最清楚:
1. 地址空间:进程之间默认隔离,A 进程改不了 B 进程的内存;线程共享进程的堆、全局变量、打开的文件,只各自保留栈、寄存器、程序计数器。
2. 通信:进程间要走管道、socket、共享内存、队列,成本高但边界清楚;线程间直接读写共享变量,快,但必须加锁,否则就是数据竞争。
3. 隔离:一个进程崩溃通常不带走别的进程;一个线程崩了(比如 C 扩展段错误)往往整进程一起没。
4. 成本:创建进程要复制/映射地址空间,更重;创建线程更轻,上下文切换也通常更便宜。Linux 里二者都是 task_struct,差别主要在是否共享 mm_struct。

连到数据工作:
- Python:GIL 让多线程几乎跑不满多核 CPU。IO 等待(读网、读盘)用线程或协程就够;纯 CPU / 压 NumPy 之外的解释器密集计算,用 multiprocessing 或多进程 Pool,每个进程一份解释器、一份 GIL。
- Spark:executor 一般是一个 JVM 进程,里面多个 task 用线程跑。进程隔离的是失败边界和内存配额;线程共享的是 JVM 堆,所以要小心广播变量、共享缓存把堆打满。
- 数仓作业:调度器起的每个 Spark/SQL 任务通常是独立进程,互不影响;进程内部再自己开线程做并行。

怎么选:要隔离、要绕开 GIL、要独立内存配额 → 多进程;要共享内存、要轻量并发、主要在等 IO → 多线程(或协程)。并发是「逻辑上同时推进」,并行才是「多个核同时在算」——多线程不等于多核并行。

口述收尾:先说「进程管资源、线程被调度」,再主动提共享地址空间和 GIL/Spark,面试官一般就不会停在概念题上。

🔍 关键词

地址空间资源分配CPU 调度GIL上下文切换

💭 面试官可能的追问

追问 1:协程和线程又有什么区别?数据任务里怎么用?

线程由 OS 调度,有内核栈,切换有系统调用成本,能真正并行(无 GIL 的语言里);协程是用户态协作式调度,自己 await 才让出,切换极轻,但一个协程阻塞整个线程。Python asyncio、Go 的 goroutine(M:N)都是这个方向。适合高并发 IO:同时打很多 HTTP、写很多连接。CPU 密集不要用协程硬扛,它不会变出多核。

追问 2:为什么 Spark 用线程跑 task,Python 算密集却常用多进程?

JVM 没有 Python 那种 GIL,多线程能吃多核,且 task 之间共享 executor 的堆、广播变量、连接池,避免每个 task 一份 JVM。Python 解释器锁住了字节码并行,算密集用线程只是交错执行;用进程才能并行。Spark 的 Python worker(pandas UDF)反而会起独立 Python 进程,就是在绕 GIL。

追问 3:线程安全的共享数据怎么处理?不加锁会怎样?

只要有「读+改+写」跨线程,就需要锁、原子变量或并发队列。不加锁会出现丢失更新、脏读、甚至结构损坏。数据场景里更稳的做法是:线程各算各的分区、最后再汇合(和 MapReduce 一样),少共享可变状态。必须共享时,优先用队列传递不可变结果,而不是多线程一起改一个 dict。

⚠️ 常见踩坑

  • 说线程有独立堆/独立地址空间,把线程说成「轻量进程」就停了
  • 默认 Python 多线程能吃满多核,忽略 GIL
  • 把并发(交替推进)和并行(多核同时算)混为一谈
  • 多线程里随便共享可变 dict/DataFrame,出数据竞争还不自知

🧠 记忆锚点

进程管资源隔离,线程共享地址空间被调度。