多线程基础回顾:进程与线程的区别、GIL锁机制、线程的创建与启动方式
各位同学好,我是你们的老朋友。今天咱们聊聊多线程的基础。说实话,这部分内容看似简单,但很多人在实际项目中栽跟头,往往就是基础没打牢。我见过不少五年经验的工程师,聊起GIL还是一脸懵。嗯,咱们从头捋一遍。
一、进程与线程:到底差在哪?
先问个问题:你打开一个浏览器,又打开一个IDE,这叫啥?这叫两个进程。那你在浏览器里开十个标签页呢?这通常是多个线程在干活。
说白了,进程是资源分配的最小单位,线程是CPU调度的最小单位。我习惯这么记:进程是房子,线程是房子里干活的人。房子(进程)有自己的地基、水电、煤气——也就是独立的内存空间。而线程呢?共享这个房子里的资源,但每个人有自己的私人物品——也就是线程私有的栈空间。
核心区别一览:
| 对比维度 | 进程 | 线程 |
|---|---|---|
| 资源开销 | 大(独立地址空间) | 小(共享地址空间) |
| 切换成本 | 高(需要切换页表) | 低(只需切换栈和寄存器) |
| 通信方式 | IPC(管道、消息队列等) | 直接读写共享变量 |
| 健壮性 | 一个进程挂了不影响其他 | 一个线程崩溃可能导致整个进程挂掉 |
我在项目中遇到过这么个事:有个同事用多进程做高并发服务,结果内存直接爆了。为啥?每个进程都加载了一份相同的模型数据。后来改成多线程,共享一份数据,内存占用直接降了70%。你想想看,选对模型有多重要。
二、Python的GIL锁:绕不开的坎
聊Python多线程,GIL是躲不开的话题。GIL全称是Global Interpreter Lock,全局解释器锁。它的存在让很多新手抓狂——说好的多线程并行呢?怎么变成了并发?
GIL的本质是什么? 它保证同一时刻只有一个线程在执行Python字节码。为什么要有这玩意儿?因为Python的内存管理不是线程安全的。CPython解释器内部大量使用引用计数,如果没有GIL,多个线程同时修改引用计数,内存就乱套了。
我的经验: 我曾经在一个爬虫项目里,用多线程抓取数据,结果发现CPU利用率只有30%。排查了半天,发现是GIL在作祟。后来换成多进程 + asyncio的组合,CPU直接跑满。所以记住:CPU密集型任务,多线程在Python里是坑;I/O密集型任务,多线程依然好用。
为什么会这样?因为I/O操作会释放GIL。当线程在等待网络响应或磁盘读写时,GIL会被释放,其他线程就能趁机执行。所以Python多线程在I/O密集场景下,效果还是不错的。
避坑指南: 我曾经天真地以为用C扩展就能绕过GIL。确实,C扩展可以手动释放GIL,但前提是你得确保C代码里没有操作Python对象。否则,轻则数据错乱,重则段错误。嗯,这里要注意,别踩坑。
三、线程的创建与启动方式
好了,理论说完了,咱们上手写代码。Python里创建线程的方式,我总结下来就三种主流玩法。
方式一:直接使用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("所有线程执行完毕")
这种方式最直接,适合简单的任务。我习惯把耗时操作封装成函数,然后扔给线程去跑。
方式二:继承Thread类
class MyThread(threading.Thread):
def __init__(self, name):
super().__init__()
self.name = name
def run(self):
print(f"线程 {self.name} 开始工作")
time.sleep(2)
print(f"线程 {self.name} 工作结束")
t1 = MyThread("A")
t2 = MyThread("B")
t1.start()
t2.start()
t1.join()
t2.join()
这种方式适合需要封装复杂逻辑的场景。你可以在类里加各种属性、方法,代码组织更清晰。我个人习惯在需要管理线程状态时用这种方式。
方式三:使用线程池
from concurrent.futures import ThreadPoolExecutor
def worker(name):
print(f"线程 {name} 开始工作")
time.sleep(2)
return f"线程 {name} 完成"
# 创建线程池,最多同时运行3个线程
with ThreadPoolExecutor(max_workers=3) as executor:
futures = [executor.submit(worker, f"Task-{i}") for i in range(5)]
for future in futures:
print(future.result())
线程池是我最推荐的方式。为啥?因为频繁创建和销毁线程是有开销的。线程池复用线程,性能更好。我在做Web服务时,几乎都用线程池来管理并发请求。
三种方式对比:
| 方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 直接使用Thread | 简单任务,少量线程 | 代码直观 | 线程管理麻烦 |
| 继承Thread类 | 需要封装状态和逻辑 | 面向对象,可扩展 | 代码稍多 |
| 线程池 | 大量任务,高并发 | 性能好,资源可控 | 需要理解池化概念 |
四、知识体系总览
说了这么多,咱们用一张图把今天的内容串起来。我画了个结构图,帮你理清思路。
这张图把今天的内容串起来了。左边是进程与线程的区别,中间是GIL这个绕不开的坎,右边是三种创建线程的方式。你想想看,掌握了这些,后面讲性能分析和内存优化,你才能跟得上。
最后说一句: 多线程不是银弹。我见过太多人一上来就开几十个线程,结果性能反而下降。记住:合适的才是最好的。先搞清楚你的任务是CPU密集还是I/O密集,再决定用多线程、多进程还是协程。
好了,基础就聊到这儿。代码示例我都跑过,放心用。有问题咱们随时交流。
可可精品资料库