刚转行第一次IT面试的时候面试官问我,list和dict是不是线程安全的。当时我就想,擦嘞,作为一个初学者list和dict不是线程安全的都看了N遍啦。这还有疑问么~~~,现在想想并没有抓住重点,线程安全应该针对于具体的操作,而不是具体的对象,我们说Queue是线程安全的是因为针对它的所有操作都是线程安全的。

官方文档

可以看到大概有这么个概念

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
L,L1,L2->list
D,D1,D2->dict
x,y->object
i,j->int
线程安全
L.append(x)
L1.append(L2)
x=L[i]
y=L.pop()
L1[i,j]=L2
L.sort()
x=y
x.field=y
D[x]=y
D1.update(D2)
D.keys()
以下是非线程安全
i=i+1
L.append(L[-1])
L[i]=L[j]
D[x]=D[x]+1

可以看到list的append啥的其实是线程安全的。我们看到的举非线程安全的例子基本都是i+=1这种,最后得到的结果小于相加次数。然后最后说一句多线程对同一资源进行操作的时候要加锁哇。。。。。这话直接说的好像比较不负责任。让我这样的初学者风声鹤唳草木皆兵

一点小解释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import threading
import dis
i = 0
def main():
global i
for _ in range(3000000):
i += 1
th = [threading.Thread(target=main), threading.Thread(target=main)]
for t in th:
t.start()
for t in th:
t.join()
print(i)
dis.dis(main)
# 4372439
# 8 0 SETUP_LOOP 30 (to 33)
# 3 LOAD_GLOBAL 0 (range)
# 6 LOAD_CONST 1 (3000000)
# 9 CALL_FUNCTION 1
# 12 GET_ITER
# >> 13 FOR_ITER 16 (to 32)
# 16 STORE_FAST 0 (_)
#
# 9 19 LOAD_GLOBAL 1 (i)
# 22 LOAD_CONST 2 (1)
# 25 INPLACE_ADD
# 26 STORE_GLOBAL 1 (i)
# 29 JUMP_ABSOLUTE 13
# >> 32 POP_BLOCK
# >> 33 LOAD_CONST 0 (None)
# 36 RETURN_VALUE

我们知道python代码经过编译成字节码指令,然后python虚拟机按照指令进行执行,这里每一条指令都是原子操作不会被中断,可以看到i+=1这条语句被划分为4条指令被执行。取出i变量的值入栈→→将被加数1入栈→→取出2个数相加结果再入栈→→结果出栈。因为是要累加。我们当然需要累加的第一步加入的值是上一个累加的结果。可是在多线程不加锁的情况下每一条指令被执行完毕后都有可能去执行另外一个线程的指令。这就会造成第一步加入的值有可能和另外一个线程是一样的,于是悲剧发生了O_o

再想一想append

1
2
3
4
5
8 19 LOAD_GLOBAL 1 (i)
22 LOAD_ATTR 2 (append)
25 LOAD_CONST 2 (1)
28 CALL_FUNCTION 1
31 POP_TOP

图片引用自
array_vs_list
可以看出list就是一个容器.append就相当与在最后面加了一个引用。虽然它也是由几条指定组成。也会发生交错执行的情况。这种交错造成的结果无非就是我本来是先执行的却在了一个的后面执行了插入,造成的结果就是顺序错乱了。😄可是特喵本来就是多线程程序,谁特喵会去关心顺序呢。所以就说线程安全了。
可以看出,append和i+=1最大的区别就是是否对自身进行了修改。dict同理~~

另外加锁的时间开销其实还是挺大的。上例,我用3个线程(结果是9000000),不加锁1.38秒,加锁执行39.16秒(python2.7.11)。不加锁1.06秒,加锁3.94秒(python3.5.1)。→_→当然你可以把i+=1改成i.apped(1)这样不用锁结果也能对了,只不过内存消耗感人

参考资料

A Python Interpreter Written in Python