一、多线程与全局解释器锁
(1)什么是线程
线程是操作系统调度和执行的最小单位,是进程内独立的执行流,可以理解为程序中正在运行的最小任务序列。其核心特点如下表:
| 核心特点 | 含义 | 具体表现 | 类比 |
|---|---|---|---|
| 轻量级 | 线程是比进程更小的执行单元,创建 / 销毁开销远小于进程 | 共享所属进程的内存、文件等资源;自身仅保存执行状态(指令指针、寄存器、栈) | 工厂里的工人:不单独建厂,只需自己的工位和工具 |
| 共享与独立 | 同进程内线程既共享资源,又各有独立执行路径 | 共享:全局变量、堆内存、打开的文件,协作效率高;独立:各自的指令指针、寄存器、栈、局部变量;共享需同步(锁、信号量)避免数据竞争 | 同一条流水线上的工人:共用厂房和设备,但各干各的活 |
| 调度与切换 | 由操作系统统一调度,实现并发 / 并行 | 按优先级调度,采用时间片轮转快速切换,切换成本低(不涉及地址空间切换) | 车间主任按优先级给工人排班,快速换岗不耽误生产 |
(2)GIL全局解释器锁
GIL(全局解释器锁)导致Python多线程在多核CPU上仅能并发而非并行,需要通过多进程才能实现正在的多核利用。无论CPU有多少个核心,在某一时刻,只允许进程中的一个线程去执行Python代码。

① 为什么设计GIL?
GIL的出现主要是为了解决CPython解释器中因多线程导致的内存管理和内部状态不安全问题。同时也是在早期单核CPU时代对开发效率和程序稳定性的一种权衡。所以GIL锁跟操作系统没关系,跟Python也没关系,它不是Python特性,只跟CPython解释器有关。
- **保证内存管理(尤其是引用计数)的线程安全:**Python 的内存管理主要依赖 "引用计数" 机制,该机制并非原子操作(包含读取、计算、写入步骤)。在多线程环境下,若多个线程同时修改对象的引用计数(如一个线程读取后,在写入前被另一线程中断并修改),极易导致计数错误(例如本该减为 0 却结果为 1),造成对象无法回收的内存泄漏或程序崩溃。GIL 通过确保同一时刻只有一个线程执行 Python 字节码,从根本上避免了此类并发冲突。
- **简化CPython的解释器实现:**在 GIL 的保护下,解释器内部的所有数据结构和状态(如当前执行的线程、字节码指令指针等)都无需额外的细粒度锁来保护,大大降低了实现复杂度。这在早期单核 CPU 是主流的时代,是一种用 "单线程执行" 换取 "实现简单和运行稳定" 的高效设计决策。
- **保护C扩展模块的完整性:**Python 的 C 扩展模块(如 NumPy、Pandas)在编写时通常假设是单线程执行的。GIL 能保护这些模块在执行时不被其他线程干扰,避免因并发操作导致其内部状态损坏,从而保证了大量现有 C 扩展的兼容性和正确性。
② 释放规则
线程争抢GIL锁,抢到锁的线程可以被执行,没有获得锁的线程只能等待。至于线程具体由哪个核心去执行,这是操作系统自己决定的。线程无法一直持有GIL锁,所以GIL有两种释放形式,主动释放和被迫释放。当线程执行I/O操作的时候主动释放锁,当线程在执行计算密集型任务超时后被迫释放锁。

二、Python中如何创建线程
(1)直接实例化Thread类
python
t = threading.Thread(target=func, name="my_thread", args=(100,), kwargs={"msg":"hello"})
直接实例化把要执行的函数传给 target,参数用 args / kwargs 传入。下面是参数说明:
| 参数 | 说明 | 取值 / 用法 |
|---|---|---|
| group | 线程组 | Python 当前版本只支持 None,一般不传 |
| target | 线程执行的目标函数 | 传入函数名,不要加括号;线程 start 后自动调用该函数 |
| name | 线程名称 | 字符串,可选,用于标识线程,不设置会自动分配默认名字 |
| args | 位置参数 | 元组,给 target 函数传位置参数;单个参数必须写逗号,如 (10,) |
| kwargs | 关键字参数 | 字典,给 target 函数传关键字参数,如 {"a":1, "b":2} |
python
# 多线程的代码
import threading, time
def add(n):
sum = 0
while sum < n:
sum += 1
print(f'sum:{sum}')
if __name__ == '__main__':
start = time.time()
n = 500000000
# 实例化线程对象
t1 = threading.Thread(target=add, args=[n / 2])
t2 = threading.Thread(target=add, args=[n / 2])
t1.start() # 启动线程
t2.start()
t1.join() # 主线程等待t1完成
t2.join()
print(f'run time: {time.time() - start}')
【注】即便有GIL,但在有IO操作的程序中,还是多线程快。
Python在进程启动起来后,会自动创建一个主线程,之后使用多线程机制可以在此基础上进行分支,产生新的子线程。子线程启动起来后,主线程默认会等待所有线程执行完成之后再退出。但是我们可以将子线程设置为守护线程,此时主线程任务一旦完成,所有子线程将会和主线程一起结束(就算子线程没有执行完也会退出)
守护线程可以在线程启动之前,通过t1.daemon = True的形式进行设置,或者在创建子线程对象时,以参数的形式指定:
python
#第一种
t1 = threading.Thread(target=add, args=[n / 2], daemon=True)
#第二种
t1 = threading.Thread(target=add, args=[n / 2])
t1.daemon = True
(2)继承Thread类
自定义子类继承 threading.Thread,重写 run (),run 里面写线程业务逻辑。
python
import threading
import time
class MyThread(threading.Thread):
def __init__(self, name, delay):
super().__init__() # 调用父类构造器
self.name = name
self.delay = delay
# 重写run:线程启动后自动执行这里
def run(self):
print(f"线程 {self.name} 开始执行")
time.sleep(self.delay)
print(f"线程 {self.name} 执行结束")
if __name__ == '__main__':
t1 = MyThread("t1", 1)
t2 = MyThread("t2", 2)
t1.start()
t2.start()
t1.join()
t2.join()
print("全部完成")
三、什么是线程安全
线程安全指多线程并发访问共享数据时,程序能正确执行、结果与单线程一致,无数据错乱或崩溃的特性。
(1)为什么会线程不安全
脏数据 :产生脏数据的原因是,当一个线程在对数据进行修改时,修改到一半时另一个线程读取了未经修改的数据并进行修改。如何避免脏数据的产生呢?一个办法就是用join方法,即先让一个线程执行完毕再执行另一个线程。但这样的本质是把多线程变成了单线程,失去了多线程的意义。另一个办法就是用锁。
(2)如何实现线程安全
**方法一:**线程同步是保证同一时刻只能有一个线程去操作全局变量。同步就是协同步调,按预定的先后次序进行运行。在Python中就是使用join让一个线程执行完毕再执行另一个线程。
**方法二:**使用同步锁或叫互斥锁。下面介绍一下互斥锁。
**互斥锁(也叫同步锁):用于锁定共享数据,保证同一时刻仅有一个线程访问操作共享资源。**互斥锁的竞争逻辑:多个线程争抢同一把锁,成功获取锁的线程优先执行;未抢到锁的线程进入等待状态,直到持有锁的线程执行完毕、释放锁资源后,其余等待线程再重新竞争锁。
在 threading 模块中提供了 Lock,调用 threading.Lock() 即可创建一把互斥锁。
python
import threading
# 创建互斥锁
lock = threading.Lock()
# 加锁
lock.acquire()
# 操作共享数据
# ...
# 释放锁
lock.release()
【注】一直等待对方释放锁的情景就是死锁。会造成应用程序的停止响应,无法处理其他任务。
四、线程与进程的区别
再次回顾,线程是进程内部的独立执行流,没有进程就没有线程。一个进程默认提供一条线程,进程可以创建多个线程。
进程之间不共享全局变量。线程之间共享全局变量,但要注意资源竞争导致线程不安全的问题,解决办法是互斥锁或者线程同步,但线程同步本质就是单线程了。创建进程的资源开销比创建线程的资源开销大。进程是操作系统资源分配的基本单位,线程是CPU调度的基本单位。线程不能独立执行,必须依存在进程中。多进程开发比单进程多线程开发稳定性更强。
| 优点 | 缺点 | |
|---|---|---|
| 进程 | 可以利用多核 CPU,适合 CPU 密集型任务;进程之间相互隔离,一个进程崩溃不会影响其他进程 | 创建、销毁、切换开销大;进程间通信复杂 |
| 线程 | 创建、销毁、切换开销小;同进程内线程通信简单 | 在 CPython 中受 GIL 限制,同一时刻只能有一个线程执行计算,无法利用多核做计算密集任务(IO 密集不受影响) |