使用dis opcode论证Python的线程安全

前言:

    群里有不少的朋友来回的问我一个问题。 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.


大家觉得文章对你有些作用! 如果想赏钱,可以用微信扫描下面的二维码,感谢!
另外再次标注博客原地址  xiaorui.cc