说实话,刚开始接触 Python 里头这 多线程 的时候,感觉挺神奇的。你想啊,同一时间让好几件事儿一块儿跑,听着就带劲儿不是?尤其那些需要等啊等、耗时间的活儿,比如从网上抓数据,或者读写大文件,总不能让程序就那么干等着吧?那时候, 多线程 就跟救星一样,能让你的程序不至于在那儿傻站着。

Python 里实现 多线程,最常用的就是标准库里那个 threading 模块了。简单粗暴地说,你就是定义一个函数,把你想让它在单独 线程 里跑的代码放进去,然后创建一个 threading.Thread 对象,把你的函数作为 target 传进去,再调用 start() 方法,砰!一个新的 线程 就启动了,它会去执行你指定的函数。

这看起来简单吧?比如:

“`python
import threading
import time

def worker(num):
“””线程要执行的任务”””
print(f’线程 {num} 开始工作啦…’)
time.sleep(1) # 模拟干活儿耗时
print(f’线程 {num} 工作完成!’)

创建两个线程

t1 = threading.Thread(target=worker, args=(1,))
t2 = threading.Thread(target=worker, args=(2,))

启动它们

t1.start()
t2.start()

等待所有线程完成

t1.join()
t2.join()

print(‘所有线程都干完啦!主线程退出。’)
“`

你看,运行这段代码,你会发现输出可能不是严格按顺序来的,有时候线程1先完成,有时候线程2先。这就是 并发 的感觉!它们在“几乎”同时进行。

但别高兴太早, Python多线程 有个绕不过去的坎儿,一个大魔王,叫 GIL,全称 Global Interpreter Lock,全局解释器锁。这玩意儿就像一个大闸门,在任何时候,只允许一个 线程CPU 上执行 Python 字节码。注意是“一个”,不管你有多少个 CPU 核,也不管你起了多少个 线程,同一时刻真正跑 Python 代码的,永远只有一个 线程

我刚学那会儿,就特纳闷,我明明开了四个 线程,我的电脑是四核的,咋CPU利用率也没上去呢?跑那种纯计算的任务,比如大量的数学运算,开 多线程 竟然比单 线程 还慢,甚至更慢!这不科学啊!后来才明白,罪魁祸首就是 GIL。当一个 线程 霸占着 GIL 跑得正欢时,别的想跑的 线程 就只能眼巴巴地等着。 线程 之间切换也需要开销,如果你的任务是纯计算, 线程 之间频繁地抢夺和释放 GIL,这开销可能比单 线程 一直跑下去还要大。

所以, Python多线程 并不是用来加速那种 CPU 密集型任务的,它主要的长项在于处理 I/O 密集型任务。什么叫 I/O 密集型?就是任务大部分时间花在等待外部操作上,比如等待网络响应、等待磁盘读写完成。

你看,当一个 线程 发起一个网络请求或者读文件时,它会进入等待状态,这时候它会“自愿”释放 GIL,让别的 线程 有机会去执行。等 I/O 操作完成了,这个 线程 再去尝试重新获取 GIL 继续执行。这样一来,当一个 线程 在等网络或者等文件的时候,其他 线程 就可以利用这段时间去做别的事情,比如处理用户请求,或者发起新的网络请求。你的程序整体效率就上去了,不会卡在那里干等。这才是 Python 多线程 的正确打开方式!处理大量网络连接、爬虫、并发下载文件,那叫一个得心应手。

好,既然多个 线程 能同时(或者说 并发 地)跑起来,那问题就来了:它们怎么打交道?尤其当它们需要访问或修改同一个数据的时候,会发生什么?想想看,两个 线程 都想往同一个银行账户里存钱,一个想存100,一个想存200。如果没有任何协调,它们可能都读取到同一个初始余额,然后各自加上自己的金额,再写回去。结果呢?账户里可能只增加了100或者200,而不是300!这就是所谓的 竞态条件(Race Condition),或者叫 数据不一致。太危险了!

为了避免这种混乱,我们就需要 同步 机制。最基本、最常用的 同步 工具就是 (Lock)。

threading.Lock 就像一个房间的门锁。谁拿到这把 ,谁才能进入房间(访问共享数据)。当一个 线程 拿到了 ,别的想拿这把 线程 就得在门外等着,直到拥有 线程release() 方法释放它。

“`python
import threading
import time

共享数据

balance = 0

lock = threading.Lock()

def deposit(amount):
global balance
# 获取锁
lock.acquire()
try:
# 在锁保护下操作共享数据
current_balance = balance
time.sleep(0.01) # 模拟操作耗时
current_balance += amount
balance = current_balance
finally:
# 释放锁,不管中间有没有出错
lock.release()

def withdraw(amount):
global balance
lock.acquire()
try:
current_balance = balance
time.sleep(0.01)
current_balance -= amount
balance = current_balance
finally:
lock.release()

模拟多个线程存取钱

threads = []
for _ in range(100):
t = threading.Thread(target=deposit, args=(1,))
threads.append(t)
t = threading.Thread(target=withdraw, args=(1,))
threads.append(t)

for t in threads:
t.start()

for t in threads:
t.join()

print(f’最终余额:{balance}’) # 如果同步正确,最终余额应该是0
“`

看到了吗?有了 lock.acquire()lock.release() 把对 balance 的操作包起来,就像给这段代码加上了“只有我一个人能进来”的通行证。这样,多个 线程 虽然都想操作 balance,但每次只有一个能拿到 进去改,别的就在外面排队,保证了数据的正确性。这是 实现 多线程 并处理共享数据的关键一步。

除了 Lockthreading 模块还有 RLock (可重入锁,同一个 线程 可以多次获取而不会死锁)、Semaphore (信号量,控制同时访问特定资源的 线程 数量)、Event (事件, 线程 可以等待某个事件发生后再继续) 等等 同步 工具,它们提供了更灵活的方式来协调 线程 间的行为。

比如 Semaphore,就特别适合那种“资源有限”的场景,比如你只想允许最多5个 线程 同时去访问某个接口,多了就会把对方服务器压垮。这时候你就可以创建一个信号量,初始值为5,每个 线程 访问前先获取一个“许可”,用完了再归还。许可发完了,新的 线程 就得等着,直到有旧的 线程 归还许可。

另外, 线程 之间交换数据,除了直接访问共享变量(需要加锁),更安全、更推荐的方式是使用 queue 模块里的各种队列。队列是 线程 安全的,也就是说,多个 线程 可以放心地往队列里放数据 (put) 或者取数据 (get),不用自己操心加锁的问题。这是一种“生产者-消费者”模型,一个 线程 生产数据放到队列里,另一个或多个 线程 从队列里取数据来消费。清晰又安全。

说了这么多,你可能觉得 Python 多线程 是个不错的选择,尤其对付 I/O 任务。但我也得跟你交个底, 多线程 编程的调试,那真不是闹着玩儿的。一旦出现 竞态条件 或者 死锁(两个或多个 线程 互相等着对方释放资源,结果谁也动不了),定位问题那叫一个酸爽,头发都抓没几根。 线程 的执行顺序本来就难以预测,加上各种 同步 问题,有时候一个 bug 只有在特定、难以重现的时机下才会暴露。所以写 多线程 代码,得小心翼翼,反复测试。

总结一下? Python 实现多线程 主要靠 threading 模块。它最适合用来处理 I/O 密集型任务,让程序在等待外部资源时也能干点别的,提高整体效率。但别指望它能打破 GIL 的限制来加速纯 CPU 计算。处理 线程 间共享数据时,切记一定要用 同步 机制,比如 (Lock),或者更优雅地使用 线程 安全的队列。虽然调试起来可能有点头疼,但一旦掌握了, 多线程 能让你写出更高效、响应更快的程序。这是每一个 Python 开发者都值得去深入折腾一下的技术。毕竟,让你的代码“活”起来, 并发 地跑起来,那种感觉,嗯,挺好的。

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。