1. 多线程基础回顾:进程与线程的区别、Python中的GIL、threading模块核心API

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

先问个问题:你写代码时,到底该用进程还是线程?

我个人习惯这样理解——进程是资源分配的最小单位,线程是CPU调度的最小单位。说白了,进程就像一家工厂,有自己的厂房、设备和原材料(内存空间、文件句柄)。线程就是工厂里的工人,共享厂房和设备,但各干各的活。

我在项目中遇到过这样的场景:一个爬虫程序,如果用多进程,每个进程都要加载一遍网络库和解析库,内存直接飙到2GB。换成多线程后,共享这些资源,内存只用了400MB。这就是共享内存空间带来的好处。

对比维度 进程 线程
内存空间 独立,互不干扰 共享同一进程的内存
创建开销 大(需要复制页表等) 小(只需分配栈空间)
通信方式 IPC(管道、队列等) 直接读写共享变量
稳定性 一个进程挂了不影响其他 一个线程崩溃可能导致整个进程挂掉
⚠️ 避坑指南:我曾经接手过一个项目,用多进程做实时数据处理。每个进程都开了数据库连接池,结果数据库连接数直接爆了。后来改成线程池+共享连接池,问题才解决。记住:进程间资源不共享,别想当然。

1.2 Python中的GIL:绕不开的坎

聊Python多线程,GIL是躲不开的话题。GIL全称Global Interpreter Lock,全局解释器锁。它保证同一时刻只有一个线程在执行Python字节码。

为什么会这样?CPython的内存管理不是线程安全的。没有GIL的话,两个线程同时操作引用计数,内存就乱套了。所以Python设计者选择了这个简单粗暴的方案。

核心结论:
  • CPU密集型任务:多线程反而更慢(锁竞争开销)
  • I/O密集型任务:多线程有效(等待I/O时会释放GIL)

我记得有一次优化一个图片处理服务。同事用了8个线程并行处理图片,结果CPU利用率只有30%。换成多进程后,8个进程直接把CPU跑满。这就是GIL的典型影响——计算密集型任务,多线程帮倒忙。

但别急着否定GIL。对于网络请求、文件读写这类I/O操作,线程在等待时会让出GIL,其他线程就能继续执行。这时候多线程的效果还是不错的。

💡 小技巧:如果你非要在Python里做并行计算,可以考虑用multiprocessing模块绕过GIL,或者用C扩展(如numpy)在底层释放GIL。

1.3 threading模块核心API:实战必备

Python的threading模块,我几乎每个项目都用。下面这几个API,你想想看,是不是经常碰到?

1.3.1 Thread:创建线程

import threading
import time

def worker(name):
    print(f"线程 {name} 开始工作")
    time.sleep(2)
    print(f"线程 {name} 结束")

# 创建线程
t1 = threading.Thread(target=worker, args=("A",))
t2 = threading.Thread(target=worker, args=("B",))

# 启动线程
t1.start()
t2.start()

# 等待线程结束
t1.join()
t2.join()

print("所有线程执行完毕")

嗯,这里要注意:join() 一定要调。不调的话,主线程可能直接退出,子线程还没跑完就被强制结束了。我在线上遇到过这种bug,查了半天才发现是忘了join。

1.3.2 Lock:线程同步

多个线程操作共享变量时,不加锁就会出问题。看这个经典例子:

counter = 0
lock = threading.Lock()

def increment():
    global counter
    for _ in range(100000):
        with lock:  # 自动获取和释放锁
            counter += 1

threads = [threading.Thread(target=increment) for _ in range(10)]
for t in threads:
    t.start()
for t in threads:
    t.join()

print(f"最终结果: {counter}")  # 应该是 1000000

用 with lock 是Python推荐的写法。它保证即使代码抛出异常,锁也会被释放。我曾经见过有人手动 acquire() 和 release(),结果某个分支忘了release,直接死锁。嗯,血的教训。

1.3.3 Event:线程间通信

Event就像一面旗子。一个线程设置旗子,其他线程等着旗子升起。

event = threading.Event()

def waiter():
    print("等待信号...")
    event.wait()  # 阻塞,直到event被set
    print("收到信号,开始干活")

def signaler():
    time.sleep(2)
    print("发送信号")
    event.set()  # 设置事件

threading.Thread(target=waiter).start()
threading.Thread(target=signaler).start()

我个人习惯用Event来做优雅关闭。比如一个后台服务线程,主线程想让它停下来时,就set一个event,服务线程检测到后自己清理退出。比直接kill优雅多了。

1.3.4 ThreadPoolExecutor:线程池

手动创建线程太麻烦,而且线程多了管理不过来。Python 3.2+ 提供了 concurrent.futures.ThreadPoolExecutor,用起来很顺手:

from concurrent.futures import ThreadPoolExecutor
import requests

def fetch_url(url):
    resp = requests.get(url)
    return len(resp.content)

urls = ["https://example.com"] * 10

with ThreadPoolExecutor(max_workers=4) as executor:
    results = list(executor.map(fetch_url, urls))

print(f"总字节数: {sum(results)}")

线程池大小怎么设?我一般遵循这个原则:I/O密集型任务,线程数设为CPU核心数的2-4倍。因为线程大部分时间在等待I/O,多开几个没关系。但别超过100,否则上下文切换开销就上来了。

本章核心知识体系:
多线程基础 进程 vs 线程 • 资源 vs 调度 • 独立 vs 共享 • 开销大 vs 开销小 GIL全局解释器锁 • 同一时刻仅一个线程执行 • CPU密集:多线程无效 • I/O密集:多线程有效 threading核心API • Thread:创建线程 • Lock:线程同步 • Event:线程通信 • ThreadPoolExecutor 理解这三块,多线程编程就有了基础 后续章节会深入实战和架构设计

好了,这一章就到这里。进程线程的区别、GIL的影响、threading的核心API,这三块是后续所有内容的基础。下一章我们开始聊线程安全和高并发设计模式。

📌 我的建议:别光看,打开IDE把代码敲一遍。尤其是那个counter的例子,试试不加锁看看结果是多少。实践出真知。

可可精品资料库