从一个示例来分析Python的线程安全问题
几行Java代码引起的思考
平时经常看到类似这样的Java代码:
x
private Integer pos = 0;
public void increase(){
// 省略掉了一些代码
synchronized (pos){
// 省略掉了一些代码
pos++;
}
}
似乎java阵营对线程安全问题极其敏感,那么对于存在GIL
的Python,我们不禁要问,Python也需要注意线程安全问题吗?Python的线程安全问题是如何产生的?该怎么处理?带着这些问题,我们用一个例子来说明。
一段结果运行总是正确的Python代码
通过创建50个线程,每个线程都对全局变量counter
执行一次自增操作,观察计算结果和期望结果是否相等。
x
import threading
import time
counter = 0
def increase():
global counter
counter += 1
if __name__ == "__main__":
threads = []
n = 50
for i in range(n):
t = threading.Thread(target=increase, args=(), name="Thread %s" % i)
t.start()
threads.append(t)
for t in threads:
t.join()
print("期望值:{}, 计算值:{}".format(n, counter))
运行结果此段代码后,得到输出:
不管执行多少遍,这个计算值总是对的,那是不是说明Python不存在线程安全问题呢?答案是否定的。我们知道Python因为GIL的存在,同一时刻,只可能有一个线程在真正执行,相应的其他线程都是出于挂起状态。那重点来了,当一个线程执行到何种程度时,才把GIL让出去给其他线程呢(得到GIL意味着得到CPU)?Python规定了两种释放GIL的情况:
- 碰到IO操作。
- 达到Python虚拟机设置的阈值。这个阈值在Python2中是100ticks(即执行了100个字节码操作),在Python3中是堵塞15ms。
显然,线程在执行increase
函数的时候,并没有触发那个阈值,也就是说所有线程都一气呵成地执行完了increase
函数,并没有被打断,所以计算出来的counter值永远是对的。
线程安全问题终于暴露出来了
既然是因为没达到阈值才导致线程没有切换,那尝试在increase
函数加点代码进去,让它达到那个阈值。修改后的increase函数代码如下:
x
def increase():
global counter
for i in range(100000):
counter += 1
counter -= 1
counter += 1
运行结果如下(python2.7):
可以看到,多次运行计算出来的值都是不正确的,说明已经发生了可怕的线程安全问题。
来看看python3下运行有什么不同:
结果显示,同样发生了线程安全问题,但只是偶尔发生,而且相差不大,说明在某些情况,Python3的阈值相对于Python2更难达到一些,所以Python3的线程切换相对于Python2更不频繁。
加锁解决资源的线程安全问题
类似与java的synchronized
关键字,Python通过threading.Lock
也可以让一段代码只能被线程同步执行。
x
lock = threading.Lock()
def increase():
global counter
lock.acquire()
for i in range(100000):
counter += 1
counter -= 1
counter += 1
lock.release()
运行结果如下:
lock.acquire()
和lock.release()
之间的代码块,只有可能被一个线程进入执行,所以不会存在counter计算混乱的问题。
总结:
Python虽然是单核多线程,但同样存在多线程安全问题。开发中要尽量避免全局共享变量的存在,实在无法避免则要注意线程安全问题,该加锁时要加锁。