从一个示例来分析Python的线程安全问题

从一个示例来分析Python的线程安全问题

几行Java代码引起的思考

平时经常看到类似这样的Java代码:

似乎java阵营对线程安全问题极其敏感,那么对于存在GIL的Python,我们不禁要问,Python也需要注意线程安全问题吗?Python的线程安全问题是如何产生的?该怎么处理?带着这些问题,我们用一个例子来说明。

 

一段结果运行总是正确的Python代码

通过创建50个线程,每个线程都对全局变量counter执行一次自增操作,观察计算结果和期望结果是否相等。

运行结果此段代码后,得到输出:

image-20190112072156853

不管执行多少遍,这个计算值总是对的,那是不是说明Python不存在线程安全问题呢?答案是否定的。我们知道Python因为GIL的存在,同一时刻,只可能有一个线程在真正执行,相应的其他线程都是出于挂起状态。那重点来了,当一个线程执行到何种程度时,才把GIL让出去给其他线程呢(得到GIL意味着得到CPU)?Python规定了两种释放GIL的情况:

  1. 碰到IO操作。
  2. 达到Python虚拟机设置的阈值。这个阈值在Python2中是100ticks(即执行了100个字节码操作),在Python3中是堵塞15ms。

显然,线程在执行increase函数的时候,并没有触发那个阈值,也就是说所有线程都一气呵成地执行完了increase函数,并没有被打断,所以计算出来的counter值永远是对的。

 

线程安全问题终于暴露出来了

既然是因为没达到阈值才导致线程没有切换,那尝试在increase函数加点代码进去,让它达到那个阈值。修改后的increase函数代码如下:

运行结果如下(python2.7):

image-20190112080312019

可以看到,多次运行计算出来的值都是不正确的,说明已经发生了可怕的线程安全问题。

来看看python3下运行有什么不同:

image-20190112080349260

结果显示,同样发生了线程安全问题,但只是偶尔发生,而且相差不大,说明在某些情况,Python3的阈值相对于Python2更难达到一些,所以Python3的线程切换相对于Python2更不频繁。

 

加锁解决资源的线程安全问题

类似与java的synchronized关键字,Python通过threading.Lock也可以让一段代码只能被线程同步执行。

运行结果如下:

image-20190112082103598

lock.acquire()lock.release()之间的代码块,只有可能被一个线程进入执行,所以不会存在counter计算混乱的问题。

 

总结

Python虽然是单核多线程,但同样存在多线程安全问题。开发中要尽量避免全局共享变量的存在,实在无法避免则要注意线程安全问题,该加锁时要加锁。