Python线程安全深度解析:GIL的局限与竞态条件
本文深入探讨Python中的线程安全概念,澄清GIL(全局解释器锁)并非万能保护伞。通过转账模拟示例,揭示在多次读写共享数据时可能出现的竞态条件,并介绍无状态函数和线程安全媒介两种实现真正线程安全的方法。
一、什么是线程安全?
线程安全这一概念直观地指向了其在多线程环境下的表现:即系统在处理并发操作时能够保持正确性和稳定性。
以加法运算为例,无论并发执行的线程数量是一千还是上万,我们期望的结果始终准确无误。例如,1+1 必须恒等于 2,而不应因线程并发导致结果变为 3 或 4 等异常值。
通常,我们使用“线程安全”来描述特定的类或函数设计。
当我们宣称某个类是线程安全的,意味着在多线程并发调用该类的方法时,不会出现违背预期的异常行为(如前述 1+1=3 的情况)。
二、在Python中有哪些类是线程安全的?
在Python中,dict、list 和 tuple 等数据结构通常被认为是线程安全的。
这得益于全局解释器锁(GIL)的保障。GIL 确保在任意时刻,只有一个线程在执行Python字节码操作。参考相关文档。

然而,这种说法往往不够严谨,容易引发误解。
接下来,我们通过一个转账场景来深入剖析。
xuewei_account = dict()
xuewei_account['amount'] = 100
# amount为负数即是转出金额
def transfer(money):
xuewei_account['amount'] += money如上代码所示,函数对 jb_account(账户)执行转入金额操作。
此处使用了 dict 类型,GIL 保证了同一时刻只有一个线程能执行对该账户的操作。
以下是多个线程并发操作的代码示例:
import random
import threading
import datetime
import time
xuewei_account = dict()
xuewei_account['amount'] = 100
# amount为负数即是转出金额
def transfer(money):
xuewei_account['amount'] += money
# 创建4个任务给重复学委账户转账
threads = []
for i in range(200):
t1 = threading.Thread(target=lambda: transfer(-1))
threads.append(t1)
t2 = threading.Thread(target=lambda: transfer(1))
threads.append(t2)
for t in threads:
t.start()
# 这次不用sleep了,用join来等待所有线程执行完毕
# join函数必须线程start后才能调用,否则出错。
for t in threads:
t.join()
print("-" * 16)
print("活跃线程数:", threading.active_count())
print("活跃线程:", threading.current_thread().name)
print("学委账户余额:", xuewei_account)
这段代码运行结果正常,因为操作是反复的 +1 和 -1,最终账户余额会恢复至初始状态。
尽管涉及多个线程,但每个线程仅对 xuewei_account 进行一次读写,此时 dict 的表现是安全的。

然而,当我们将对 dict 的赋值修改操作增多(特别是线程内反复获取值并修改)时,情况如下:
import random
import threading
import datetime
import time
xuewei_account = dict()
xuewei_account['amount'] = 100
# amount为负数即是转出金额
def transfer(money):
for i in range(100000):
xuewei_account['amount'] = xuewei_account['amount'] + money
# 创建400个任务重复给学委账户转账
threads = []
for i in range(200):
t1 = threading.Thread(target=lambda: transfer(-1))
threads.append(t1)
t2 = threading.Thread(target=lambda: transfer(1))
threads.append(t2)
for t in threads:
t.start()
for t in threads:
t.join()
print("-" * 16)
print("活跃线程数:", threading.active_count())
print("活跃线程:", threading.current_thread().name)
print("学委账户余额:", xuewei_account)这是某一次运行的结果(账户数值每次运行可能不同):

由此可见,dict 无法承受多个线程反复的写操作,数据一致性遭到破坏。
关键区别在于:每个线程对 xuewei_account 进行了大量读写。虽然 dict 本身是线程安全的,但多个线程穿插修改 account 时,程序方法栈中出现了操作旧值的情况(见下图)。
问题主要出在下面这段代码逻辑中:
xuewei_account[‘amount'] += money
# 即是 xuewei_account[‘amount'] = xuewei_account[‘amount']+ money进一步抽象简化,可表示为:
a = a + b
每个线程都执行 +b 操作,理论上最终 a 的值应为 a+2b。
上述操作揭示了以下并发问题:

在某个线程 T1 获取 a 的值后,准备加上 b。
与此同时,另一个线程 T2 已完成 a+b 操作,将 a 更新为 a+b。
然而,T1 随后拿着之前获取的旧 a 值再次执行 a+b 操作,导致 a 被重新赋值为 a+b。
这导致少加了一个 b,最终结果本应是 a+2b,却变成了 a+b(因为 T1 使用了旧值,而 T2 的修改在 T1 继续执行前已完成)。
当然,实际多线程之间的交互比图示更为随机和复杂。
三、如何做到真正线程安全?
dict 读取数据虽线程安全,但反复读写极易导致数据混乱。
若我们要设计一个线程安全的函数,它必须不涉及任何共享变量,或者是完全没有状态依赖的函数。
def thread_safe_method():
pass1.无状态函数
例如下面的加法函数,无论多少个线程调用,返回值始终符合预期的 a+b。
def add(a, b):
return a + b2.另一种化繁为简的方法
我们可以尝试将多线程转换为单线程处理,这需要借助一个线程安全的媒介。
到此这篇关于Python线程之认识线程安全 的文章就介绍到这了,更多相关认识Python线程安全 内容请搜索本站以前的文章或继续浏览下面的相关文章希望大家以后多多支持本站!
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









