【学习心得】Python多线程

一、多线程与全局解释器锁

(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 密集不受影响)
相关推荐
happylifetree5 小时前
Python70:核心语法-函数进阶-案例2
python
王中阳Go5 小时前
两个入口答案不一致先别调模型:6 行入口口径对账表(附脚本)
人工智能·后端·python·大语言模型·1024程序员节
BK贝壳5 小时前
bool-hybrid-array 落盘我只认 .bha:用 Create_BHA / Ask_BHA 做布尔数组序列化
数据结构·python·numpy·1024程序员节
Sakura_j_h_6 小时前
Python 在定时任务里输出 emoji,UnicodeEncodeError 从哪来
开发语言·字符编码·windows·python·定时任务
supabc1237 小时前
欧盟电子发票(XRechnung/Peppol/ZUGFeRD)踩坑记:用 Python 不装 Java 跑官方校验规则
xml·python·开源·电子发票·1024程序员节
ZhengEnCi8 小时前
AI Agent工具调用防护层怎么写-一份能直接跑的Python四层拦截实现
python·llm·agent
ZhengEnCi8 小时前
AI Agent工具误调用怎么解决-从参数校验到高危二次确认的四层工程防线
python·llm·agent
镜子AI9 小时前
镜子AI工程教程:用Python实现关键词布局检查器与语义改写查重(附完整代码)
人工智能·python·microsoft
小oo呆10 小时前
【学习心得】Python协程
python