4 多线程抢夺同一个变量¶
多线程编程,存在抢夺同一个变量的问题。
比如下面例子,创建的10个线程同时竞争全局变量
a
:import threading
a = 0
def add1():
global a
a += 1
print('%s adds a to 1: %d'%(threading.current_thread().getName(),a))
threads = [threading.Thread(name='t%d'%(i,),target=add1) for i in range(10)]
[t.start() for t in threads]
执行结果:
t0 adds a to 1: 1
t1 adds a to 1: 2
t2 adds a to 1: 3
t3 adds a to 1: 4
t4 adds a to 1: 5
t5 adds a to 1: 6
t6 adds a to 1: 7
t7 adds a to 1: 8
t8 adds a to 1: 9
t9 adds a to 1: 10
结果一切正常,每个线程执行一次,把a
的值加1,最后a
变为10,一切正常。
运行上面代码十几遍,一切也都正常。
所以,我们能下结论:这段代码是线程安全的吗?
NO!
多线程中,只要存在同时读取和修改一个全局变量的情况,如果不采取其他措施,就一定不是线程安全的。
尽管,有时,某些情况的资源竞争,暴露出问题的概率极低极低
:
本例中,如果线程0 在修改a后,其他某些线程还是get到的是没有修改前的值,就会暴露问题。
但是在本例中,a = a + 1
这种修改操作,花费的时间太短了,短到我们无法想象。所以,线程间轮询执行时,都能get到最新的a值。所以,暴露问题的概率就变得微乎其微。
5 代码稍作改动,叫问题暴露出来¶
只要弄明白问题暴露的原因,叫问题出现还是不困难的。
想象数据库的写入操作,一般需要耗费我们可以感知的时间。
为了模拟这个写入动作,简化期间,我们只需要延长修改变量a
的时间,问题很容易就会还原出来。
import threading
import time
a = 0
def add1():
global a
tmp = a + 1
time.sleep(0.2) # 延时0.2秒,模拟写入所需时间
a = tmp
print('%s adds a to 1: %d'%(threading.current_thread().getName(),a))
threads = [threading.Thread(name='t%d'%(i,),target=add1) for i in range(10)]
[t.start() for t in threads]
重新运行代码,只需一次,问题立马完全暴露,结果如下:
t0 adds a to 1: 1
t1 adds a to 1: 1
t2 adds a to 1: 1
t3 adds a to 1: 1
t4 adds a to 1: 1
t5 adds a to 1: 1
t7 adds a to 1: 1
t6 adds a to 1: 1
t8 adds a to 1: 1
t9 adds a to 1: 1
看到,10个线程全部运行后,a
的值只相当于一个线程执行的结果。
下面分析,为什么会出现上面的结果:
这是一个很有说服力的例子,因为在修改a前,有0.2秒的休眠时间,某个线程延时后,CPU立即分配计算资源给其他线程。直到分配给所有线程后,根据结果反映出,0.2秒的休眠时长还没耗尽,这样每个线程get到的a值都是0,所以才出现上面的结果。
以上最核心的三行代码:
tmp = a + 1
time.sleep(0.2) # 延时0.2秒,模拟写入所需时间
a = tmp