Python的原子性操作是如何实现的

翻阅 Python文档 可以得知,Python中某些操作如 list.append(item) 是原子性操作,在多个线程对一个列表执行添加是线程安全的。

Python的原子性操作是如何实现的


大致翻译如下

什么样的全局值突变是线程安全的?

全局解释器锁(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+1

L.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 个字节码协作完成:

  1. LOAD_NAME 将 a 当前的值加载进运行栈;
  2. LOAD_CONST 将常量 1 加载到运行栈;
  3. INPLACE_ADD 对栈上两个操作数进行加法运算;
  4. 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

回到顶部