废话不多说,先来看下面,代码,预测一下运行结果:
代码1-1
from concurrent import futures
def _add(loop=1):
global number
for _ in range(loop):
number += 1
def _sub(loop=1):
global number
for _ in range(loop):
number -= 1
if __name__ == "__main__":
loop_time = 1E7
number = 0
with futures.ThreadPoolExecutor(max_workers=2)as pool:
future1 = pool.submit(_add, loop_time)
future2 = pool.submit(_sub, loop_time)
print(number)
答案揭晓,无论你是用哪个版本的Python, 其结果都为0
如果你感到疑惑,不妨将第_add或_sub函数中的循环次数改动一下,使得两个函数的循环次数不对等,如下所示:
代码1-2
from concurrent import futures
def _add(loop=1):
global number
for _ in range(loop):
number += 1
def _sub(loop=1):
global number
for _ in range(loop-1000):
number -= 1
if __name__ == "__main__":
loop_time = 1E7
number = 0
with futures.ThreadPoolExecutor(max_workers=2)as pool:
future1 = pool.submit(_add, loop_time)
future2 = pool.submit(_sub, loop_time)
print(number)
运行结果依然是0!!
有伙伴就要吃惊了,如果在Python中, builtin的写入数据是线程安全的,上述结果理应是1000才对啊! 后来经过我的排查发现,实际问题出在第17行。 让我来考考你:
loop_time = 1E7
print(type(loop_time))
loop_time是什么数据类型呢?
。。。。。。。。。。。。。。。。。。。。
答案是: <class 'float'>
这也就意味着当你运行下述代码:
for _ in range(1E7):
pass
你会发现,报错!
那么,为什么在开启的子线程中,这样的报错却没有显示出来呢?
这正是线程执行的第一大坑,线程报错不会显示出来,而是提前终止了线程! 这也就意味着,在线程中的for循环,压根没有执行!!!
好的,让我们给17行代码加上:
loop_time = int(1E7)
现在再运行最开始的代码1-1, 你猜猜其结果是什么?
。。。。。。。。。。。。。。。。。。。。。
答案是,与Python解释器有关!!!
如果你是在Python3.8后的版本运行1-1修正后的代码,你会发现无论你怎么运行,number的值都为0!!
反之,如果你是在Python3.8之前的版本运行上述代码,number的值是不确定的,它甚至可以是负数!
Problem1: 为什么number的结果不定呢?
ans: 这是由于在Python3.8之前的版本中,很多builtin的写入操作并不是线程安全的;一个简单的代码:
a += 1
它事实上是包含至少两步的。第一步就是对数据进行读取,确定了这个数据的值后,再对这个值进行++; 下面是一个简化理解的代码:
# a += 1类似于:
temp = a
temp = temp + 1
a = temp
这也就意味着,在多线程环境中,为简化问题假设只有两个线程A, B
假设a的初始值为0, A线程和B线程都希望给a+=1;
①但A线程执行到a += 1时, 实际对应执行到Cpython中读取数据那一步,此时读取到a = 0,准备+1;
②与此同时,B线程已经快人一步,在读取完a = 0后,执行完了a += 1的操作;
③随后切换到A线程执行 a = 0 + 1的操作。
结果可想而知,a的值不再是预期的2, 而是1 !!!
这便是多线程中最臭名昭著的竞态问题!!
problem2: 为什么python3.8后这个builtin同时写入的问题就不存在了呢?
有了problem1的铺垫,这就极好理解了。Python3.8后为builtin的写入操作加锁了,往后的写入操作都变成了原子性操作,不会出现读一半还没写入就被别人抢先一步写入的情况;
我们简单看看kimi的解释:
"""
实际上,从Python 3.8开始,CPython(Python的官方实现)引入了对多线程更好的支持,特别是在原子操作方面。这意味着在Python 3.8及更高版本中,一些操作(如对简单全局变量的+=操作)在多线程环境中是原子的,因此在这些版本中,你的代码会输出预期的0。
在内部,这种原子性可能是通过锁或其他同步机制实现的,但这是CPython的内部实现细节,对于Python程序员来说是透明的。
"""
总结??? 懒得总结。评论区留下你的疑问与见解