Python的原子性操作是如何实现的
翻阅 Python文档 可以得知,Python中某些操作如 list.append(item)
是原子性操作,在多个线程对一个列表执行添加是线程安全的。
大致翻译如下
什么样的全局值突变是线程安全的?
全局解释器锁(GIL)在内部使用,以确保每次只有一个线程在pythonvm中运行。一般来说,Python只提供在字节码指令之间的线程间切换;它的切换频率可以通过sys.setswitchinterval()
来改变。 因此,从Python程序的角度来看,每个字节码指令以及从每个指令的所有用C实现的代码都是原子性的。
从理论上讲,这意味着精确的计算需要对PVM字节码的实现有一个准确的理解。实际上,这意味着对内置数据类型(int、list、dict等)的共享变量的操作实际上是“原子”的。
以下是原子性的:
L.append(x)L1.extend(L2)
x = L[i]
x = L.pop()
L1[i:j] = L2
L.sort()
x = y
x.field = y
D[x] = y
D1.update(D2)
D.keys()
以下不是:
i = i+1L.append(L[-1])
L[i] = L[j]
D[x] = D[x] + 1
翻译结束
我的疑惑是
,Python是如何将诸如“删除列表中一个元素”
,“给一个整型变量赋值”
,“给列表添加一个元素”
的操作封装成具有原子性
的操作。
按道理,以上操作应该都经历读取地址
,写入数据
的过程,在 GIL 的作用下,Python中每次只允许一个线程工作。
假设以上操作不是原子性的,那么每个线程的分片用完
时有可能只是才执行完读取地址
的阶段,还没来得及写入数据
就轮到下一个线程了,这样就线程不安全了,但是实际上并不会发生这种情况。
所以会不会是Python对以上操作执行添加了PV操作
呢?或者是加了个锁
?
但是问题又来了,如果仅仅是单线程处理
,或者是对数组切片并多线程处理
,这岂不是非常损耗性能?
以上便是我的困惑与思考,希望能得到解答,十分感谢啦~
回答:
GIL保证字节码级别的原子性和线程安全性,因此当个字节码执行一定是安全的,执行结果一定是一致的。
而有些操作,底层需要通过多个字节码来完成,这样的操作就不是原子的,因此不是线程安全的。举个例子,a+=1
。反编译这个语句,发现它由4个字节码组成:
>>> dis.dis(compile('a+=1', '', 'exec')) 1 0 LOAD_NAME 0 (a)
2 LOAD_CONST 0 (1)
4 INPLACE_ADD
6 STORE_NAME 0 (a)
8 LOAD_CONST 1 (None)
10 RETURN_VALUE
这个简单的语句,背后需要 4 个字节码协作完成:
- LOAD_NAME 将 a 当前的值加载进运行栈;
- LOAD_CONST 将常量 1 加载到运行栈;
- INPLACE_ADD 对栈上两个操作数进行加法运算;
- STORE_NAME 将计算结果保存;
如果你学过汇编的话,你会发现Python字节码跟汇编指令非常像!GIL保证当个字节码的执行不会受到其他线程的任何干扰,但是任何字节码间都可能发生线程切换。
假设两个线程同时自增变量a,a当前值为0;线程A执行到第3步,自增结果1已算出,但未保存;这时线程B得到调度开始执行,同样算出结果1并抢先保存了;A回过头来将结果1保存,B的结果被覆盖了,最终a的值是1。然而,两个线程对a自增,它的值讲道理应该是2!这就是并发操作产生的竞争态,解决方法是用一个锁将这几个字节码作为原子操作保护起来。
我写过一个关于Python虚拟机内部实现的专栏Python源码深度剖析,里面有些许介绍,有兴趣可以看看~
以上是 Python的原子性操作是如何实现的 的全部内容, 来源链接: utcz.com/a/34330.html