一、线程基础概念:进程与线程的区别、线程的生命周期、Python中的GIL全局解释器锁

1.1 进程与线程:到底差在哪?

很多初学者问我,进程和线程到底有什么区别?我习惯用一个比喻来解释:进程就像一家餐厅,线程就是里面的厨师。

餐厅有自己的场地、设备、食材(内存空间、文件句柄、系统资源)。每个厨师共享这些资源,但各自负责不同的菜品。厨师之间可以随时交流,共用一把菜刀(共享数据)。

但餐厅之间是隔离的。你不可能跑到隔壁餐厅去拿他们的酱油。这就是进程的边界——每个进程拥有独立的地址空间。

核心区别一览:

对比维度 进程 线程
资源开销 大(独立内存、文件表等) 小(共享进程资源)
通信方式 IPC(管道、消息队列、共享内存) 直接读写共享变量
切换成本 高(需要切换页表、刷新TLB) 低(只需保存寄存器)
健壮性 一个进程挂了不影响其他进程 一个线程崩溃可能导致整个进程挂掉

我在项目中遇到过这样一个场景:一个后台服务需要同时处理1000个WebSocket连接。如果用进程,光内存开销就扛不住。用线程,每个连接只占几KB的栈空间,轻松搞定。但代价是什么?线程安全问题——后面我们会详细讲。

1.2 线程的生命周期:从出生到死亡

线程不是凭空出现的。它有自己的生命周期。我习惯把它分成五个阶段:

  1. 新建(New)——线程对象刚创建,还没启动。就像招了个厨师,还没让他上灶。
  2. 就绪(Runnable)——调用了start(),线程已经准备好,等待CPU分配时间片。厨师已经站在灶台前,等锅热起来。
  3. 运行(Running)——CPU开始执行线程的run()方法。厨师开始颠勺炒菜。
  4. 阻塞(Blocked/Waiting)——线程因为等待I/O、获取锁、调用sleep()等原因暂停。厨师发现没盐了,等着采购员送过来。
  5. 终止(Terminated)——run()执行完毕,或者抛出未捕获异常。厨师下班了。

避坑指南:我曾经犯过一个低级错误——调用了线程的run()而不是start()。结果run()在当前线程同步执行,根本没创建新线程。你想想看,这相当于让厨师长亲自去切菜,其他菜全耽误了。记住:start()才是启动线程的正确姿势。

1.3 Python的GIL:绕不开的"紧箍咒"

说到Python多线程,就不得不提GIL(Global Interpreter Lock,全局解释器锁)。很多Python开发者一听到这个词就头疼。说实话,我刚接触时也觉得这是个设计缺陷。

GIL是什么?简单说,CPython解释器里有一把大锁。任何时刻,只有一个线程能执行Python字节码。其他线程想执行?等着吧。

GIL的影响:

  • CPU密集型任务:多线程反而可能比单线程慢(锁竞争开销)
  • I/O密集型任务:多线程依然有效(等待I/O时会释放GIL)
  • 数值计算、图像处理:建议用多进程或C扩展

为什么会这样?我解释一下历史原因。Python早期设计时,内存管理用的是引用计数。如果没有GIL,多个线程同时修改对象的引用计数,会导致内存泄漏甚至崩溃。说白了,GIL是用性能换安全。

我在项目中遇到过真实案例:一个爬虫程序,用多线程抓取100个网页。按理说应该快10倍,结果只快了2倍。为什么?因为解析HTML时CPU密集,GIL成了瓶颈。后来改成多进程+异步I/O,速度直接起飞。

注意:GIL只存在于CPython。如果你用Jython或IronPython,就没有这个限制。但现实是,99%的Python项目都用CPython。所以,别指望绕过GIL,学会与它共存。

1.4 一张图看懂本章知识体系

线程基础概念知识体系 进程 vs 线程 • 进程:独立资源,隔离性强 • 线程:共享资源,轻量级 • 切换成本:进程高,线程低 • 通信:IPC vs 共享变量 线程生命周期 • 新建(New) • 就绪(Runnable) • 运行(Running) • 阻塞(Blocked) • 终止(Terminated) GIL全局解释器锁 • 同一时刻仅一个线程执行 • CPU密集:多线程反效果 • I/O密集:依然有效 • 替代方案:多进程/C扩展 核心要点总结 1. 线程是轻量级进程,共享进程资源,适合I/O密集型任务 2. 线程生命周期:新建→就绪→运行→阻塞→终止 3. GIL是CPython的"双刃剑"——保证安全但限制并行 4. 选型建议:CPU密集用多进程,I/O密集用多线程/异步

1.5 代码示例:感受线程的创建与GIL的影响

import threading
import time

def cpu_intensive_task(n):
    """CPU密集型任务:计算斐波那契数列"""
    if n <= 1:
        return n
    return cpu_intensive_task(n-1) + cpu_intensive_task(n-2)

def io_intensive_task():
    """模拟I/O密集型任务:等待+打印"""
    time.sleep(1)
    print(f"线程 {threading.current_thread().name} 完成I/O操作")

# 测试CPU密集型(单线程 vs 多线程)
start = time.time()
for _ in range(4):
    cpu_intensive_task(35)
print(f"单线程CPU耗时: {time.time() - start:.2f}s")

start = time.time()
threads = []
for _ in range(4):
    t = threading.Thread(target=cpu_intensive_task, args=(35,))
    threads.append(t)
    t.start()
for t in threads:
    t.join()
print(f"多线程CPU耗时: {time.time() - start:.2f}s")

# 测试I/O密集型(单线程 vs 多线程)
start = time.time()
for _ in range(4):
    io_intensive_task()
print(f"单线程I/O耗时: {time.time() - start:.2f}s")

start = time.time()
threads = []
for _ in range(4):
    t = threading.Thread(target=io_intensive_task)
    threads.append(t)
    t.start()
for t in threads:
    t.join()
print(f"多线程I/O耗时: {time.time() - start:.2f}s")

运行结果预测:

  • CPU密集型:单线程和多线程耗时差不多(甚至多线程更慢)
  • I/O密集型:多线程比单线程快3-4倍

这就是GIL的"偏心"——它只卡CPU,不卡I/O。嗯,记住这个特性,以后选型时心里就有数了。

1.6 避坑指南:我踩过的那些坑

最后分享几个实战教训:

  • 别用time.sleep(0)来让出GIL——我曾经这么干过,结果CPU占用率飙升。正确做法是用threading.Event或条件变量。
  • 线程数不是越多越好——我见过有人开500个线程抓数据,结果上下文切换开销比实际工作还大。一般I/O密集型线程数=CPU核心数×2就够了。
  • 守护线程要小心——设置daemon=True的线程,主线程退出时会强制终止。如果线程正在写文件,数据可能丢失。我习惯用join()等待所有线程完成。

好了,这一章就到这里。线程的基础概念是后续所有并发编程的基石。把进程和线程的区别、生命周期、GIL的影响搞明白,后面学锁、条件变量、线程池才会顺手。

可可精品资料库