前言:
群里有不少的朋友来回的问我一个问题。 python线程不是有gil么? 为毛还说计数不是线程安全的么? list, set, dict 是线程安全么? 先说下答案, 计数不是原子的, list, set, dict 在python里是原子操作。
那么什么是原子操作?硬件的原子说的是 cpu指令集, 软件的原子可以理解为并发控制,不可被中断,加锁解决。 这也可以理解为线程安全问题。 叫法不同而已,无所谓的。。。 更多人会叫线程安全。
该文章写的有些乱,欢迎来喷 ! 另外文章后续不断更新中,请到原文地址查看更新。 http://xiaorui.cc/?p=4637
很多人其实不知道原因,大多是听人说的罢了。 首先我们可以肯定在python多线程下计数不是安全的。
我们可以用dis来论证下,python内置的dis是一个很棒的查看opcode字节码指令的模块。
# xiaorui.cc from dis import dis a = 0 def counter(): global a a += 1 print dis(counter)
Python字节码输出:
# xiaorui.cc 0 LOAD_GLOBAL 0 (a) 3 LOAD_CONST 1 (1) 6 INPLACE_ADD 7 STORE_GLOBAL 0 (a) 10 LOAD_CONST 0 (None) 13 RETURN_VALUE
LOAD_GLOBAL加载全局变量a,LOAD_CONST加载右面的常量数据, INPLACE_ADD加法指令,a + 1, 然后用STORE_GLOBAL赋值回去。
一个计数用了三个字节码指令, 一个是读取变量,一个是加法运算,最后一个是赋值过去。。。 线程A取到变量,然后加法运算,但是被内核的进程调度打断,执行单元被抢占…. 线程B也去执行同样的方法,但是他顺利执行完毕了,这时候线程A要继续他的变量赋值。。。 但明显该数据发生变化了…
另外我们知道Python是没有volatile关键字的,原因后面有说,但是在java等语言里是有线程在cpu缓存这么一说。多个线程很大几率是跑在两个cpu core上,那么每个线程都会把获取到的变量缓存起来,那么随着时间一长,线程之间的数据的变化差距就有了。 什么时候会发生同步? 根据jvm策略或发生中断时,同步主存数据。 Python取消了volatile这样的内存屏障方法,因为他有GIL全局锁… 每次只有一个线程同时在跑,另一个线程run起来的时候,必然会触发cs,保证每次堆里面的数据不是缓存…
另外cpu是有三级缓存的,最靠近cpu的是L1,远点的是L3, 最慢的是 主存,也就是内存。。。 线程上下文信息缓存位是从L1 –> L2 –> L3 –> main memory !!!
在python下计数不是线程安全的。那么容器类的数据结构是安全的么?
是,安全的 !!!
# xiaorui.cc from dis import dis dis(compile('del d[k]', 'example', 'exec')) d = {"a": 123, "b": 999} def pop(): global d del d["a"] dis(pop)
输出:
3 0 LOAD_GLOBAL 0 (d) 3 LOAD_CONST 1 ('a') 6 DELETE_SUBSCR 7 LOAD_CONST 0 (None) 10 RETURN_VALUE
Python官方文档中和Cpython源代码中都是有说明的。
还有一个问题,python yield是线程安全的么? 我们首先可以明确生成器是迭代器的一种,多个线程针对一个生成器进行请求会发生什么?
比如一个 list ,用yield来构建生成器来返回数据 . 不同的可迭代对象有自己的实现方法。比如 一个 列表,他的迭代器是通过下标来获取数据的。 从左到右。 怎么走? 那还是计数!!!
真的是这样么? 不…. Cpython比我们想的都要粗暴….
# xiaorui.cc from dis import dis def go(): mylist = ['aa', 'bb', 'c'] for i in mylist: yield i dis(go)
输出:
2 0 LOAD_CONST 1 ('aa') 3 LOAD_CONST 2 ('bb') 6 LOAD_CONST 3 ('c') 9 BUILD_LIST 3 12 STORE_FAST 0 (mylist) 3 15 SETUP_LOOP 19 (to 37) 18 LOAD_FAST 0 (mylist) 21 GET_ITER >> 22 FOR_ITER 11 (to 36) 25 STORE_FAST 1 (i) 4 28 LOAD_FAST 1 (i) 31 YIELD_VALUE 32 POP_TOP 33 JUMP_ABSOLUTE 22 >> 36 POP_BLOCK >> 37 LOAD_CONST 0 (None) 40 RETURN_VALUE
单纯的看指令会发现JUMP一直循环的调用FOR_ITER, FOR_ITER内部在不停的触发next()方法,每个可迭代对象有自己的next实现。 真正操作取值的指令是FOR_ITER,GIL保证单个opcode是安全的。
那么我们继续深入Cpython的ceval.c,发现对yield操作是不会释放gil锁,就算被内核进程调度走了,其他线程还是拿不到gil锁,trylock之后,拿到一个 generator already executing error信息。
下面是 strace 到的python多线程并发调用生成器对象的信息…. 你看到了什么? 这么线程在抢锁!!! gil作为解释器锁保证只有一个线程在跑,且粒度在于单个opcode字节码。
[pid 18940] futex(0x10ba570, FUTEX_WAKE_PRIVATE, 1 <unfinished ...> [pid 18939] futex(0x10ba570, FUTEX_WAKE_PRIVATE, 1 <unfinished ...> [pid 18941] <... futex resumed> ) = 0 [pid 18940] <... futex resumed> ) = 1 [pid 18939] <... futex resumed> ) = 0 [pid 18941] futex(0x10ba570, FUTEX_WAKE_PRIVATE, 1 <unfinished ...> [pid 18940] futex(0x10ba570, FUTEX_WAIT_PRIVATE, 0, NULL <unfinished ...> [pid 18941] <... futex resumed> ) = 0 [pid 18940] <... futex resumed> ) = -1 EAGAIN (Resource temporarily unavailable) [pid 18939] futex(0x10ba570, FUTEX_WAKE_PRIVATE, 1 <unfinished ...> [pid 18940] futex(0x10ba570, FUTEX_WAIT_PRIVATE, 0, NULL <unfinished ...> [pid 18941] futex(0x10ba570, FUTEX_WAKE_PRIVATE, 1 <unfinished ...> [pid 18940] <... futex resumed> ) = -1 EAGAIN (Resource temporarily unavailable) [pid 18941] <... futex resumed> ) = 0 [pid 18939] <... futex resumed> ) = 0 [pid 18941] futex(0x10ba570, FUTEX_WAIT_PRIVATE, 0, NULL <unfinished ...> [pid 18940] futex(0x10ba570, FUTEX_WAKE_PRIVATE, 1 <unfinished ...> [pid 18939] futex(0x10ba570, FUTEX_WAKE_PRIVATE, 1 <unfinished ...> [pid 18941] <... futex resumed> ) = 0 [pid 18940] <... futex resumed> ) = 1 [pid 18939] <... futex resumed> ) = 0 [pid 18941] futex(0x10ba570, FUTEX_WAKE_PRIVATE, 1 <unfinished ...> [pid 18940] futex(0x10ba570, FUTEX_WAIT_PRIVATE, 0, NULL <unfinished ...> [pid 18941] <... futex resumed> ) = 0 [pid 18940] <... futex resumed> ) = -1 EAGAIN (Resource temporarily unavailable) [pid 18939] futex(0x10ba570, FUTEX_WAKE_PRIVATE, 1 <unfinished ...> [pid 18940] futex(0x10ba570, FUTEX_WAIT_PRIVATE, 0, NULL <unfinished ...> [pid 18941] futex(0x10ba570, FUTEX_WAKE_PRIVATE, 1 <unfinished ...> [pid 18940] <... futex resumed> ) = -1 EAGAIN (Resource temporarily unavailable)
正常项目中不可能会出现多线程调用同一个生成器对象…
END.