专栏名称: dwzb
目录
相关文章推荐
51好读  ›  专栏  ›  dwzb

列表与队列——谈谈线程安全

dwzb  · 掘金  ·  · 2018-06-02 13:38

正文

列表与队列——谈谈线程安全

本文首发于 知乎

关键词:线程安全、GIL、原子操作(atomic operation)、存储数据类型(List、Queue.Queue、collections.deque)

当多个线程同时进行,且共同修改同一个资源时,我们必须保证修改不会发生冲突,数据修改不会发生错误,也就是说,我们必须保证线程安全。

同时我们知道,python中由于GIL的存在,即使开了多线程,同一个时间也只有一个线程在执行。

那么这是否就说明python中多个线程执行时,不会发生冲突呢?答案是否定的。

GIL下的线程不安全

来看下面这段代码

import threading
import time
zero = 0
def change_zero():
    global zero
    for i in range(3000000):
        zero += 1
        zero -= 1

th1 = threading.Thread(target = change_zero)
th2 = threading.Thread(target = change_zero)
th1.start()
th2.start()
th1.join()
th2.join()
print(zero)

两个线程共同修改 zero 变量,每次对变量的操作都是先加1再减1,按理说执行3000000次, zero 结果应该还是0,但是运行过这段代码发现,结果经常不是0,而且每次运行结果都不一样,这就是数据修改之间发生冲突的结果。

其根本原因在于, zero += 1 这一步操作,并不是简单的一步,而可以看做两步的结合,如下

x = zero + 1
zero = x

所以可能在一个线程执行时,两步只执行了一步 x = zero + 1 (即还没来得及对zero进行修改),GIL锁就给了另一个线程(不熟悉锁的概念以及GIL的可以先看 这篇文章 ),等到GIL锁回到第一个线程,zero已经发生了改变,这时再执行接下来的 zero = x ,结果就不是对 zero 加1了。一次出错的完整的模拟过程如下

初始:zero = 0
th1: x1 = zero + 1  # x1 = 1
th2: x2 = zero + 1  # x2 = 1
th2: zero = x2      # zero = 1
th1: zero = x1      # zero = 1  问题出在这里,两次赋值,本来应该加2变成了加1
th1: x1 = zero - 1  # x1 = 0
th1: zero = x1      # zero = 0
th2: x2 = zero - 1  # x2 = -1
th2: zero = x2      # zero = -1
结果:zero = -1

为了更好地说明python在GIL下仍存在线程不安全的原因,这里需要引入一个概念: 原子操作(atomic operation)

原子操作

原子操作,指不会被线程调度机制打断的操作,这种操作一旦开始,就一直运行到结束,中间不会切换到其他线程。







请到「今天看啥」查看全文