Python部落(python.freelycode.com)组织翻译,禁止转载,欢迎转发。
与同事的一次对话使我意识到一个事实,那就是Python中相当大一部分操作都是原子的,即使像字典和类成员赋值这样的操作也是原子的。
为了完成像哈希表插入这样的操作,需要执行很多条机器语言指令,我很难想象这个操作居然是原子的。
为什么会这样?
Python FAQ提供了解释以及原子操作的完整列表,但简短的答案是:
Python字节码解释器只有在一个机器指令完成后,另一个机器指令没开始前,才会进行线程切换。
全局解释器锁(GIL)只允许一次执行一个线程。
很多操作都被转换为单个字节码指令。
使用dis包可以很容易的查看一个操作是否编译成单个字节码指令。
那么注意事项是什么? 依靠原子性而不是使用锁是否安全?
首先,上面的链接FAQ并没有说明这种行为多大程度上被认为是Python规范的一部分,还是CPython实现的情况。 它取决于GIL,所以在GIL-less Pythons(IronPython,Jython,PyPy-TM)上可能是不安全的。 在使用GIL(PyPy)的非CPython实现上安全吗? 我当然可以想象有些优化可能会使这些操作的原子性无效。
其次,即使不是绝对必要的,锁也提供了明确的线程安全保证,并且可以作为代码访问共享内存的有用说明。 如果没有锁,必须小心,因为很容易误把非原子操作假设成原子操作(postmortem 示例:Python的swap不是原子操作)。 一个明确的备注可能也是必要的,让合作者不必产生“等等,这可能需要一个锁!”的反应。
第三,因为Python允许重载如此多的内建方法,所以有些情况下这些操作不再是原子的。 Google Python风格指南建议:
不要依赖于内置类型的原子性。
虽然Python的内置数据类型(如字典)似乎具有原子操作,但是在某些情况下它们不是原子的(例如,如果将hash或eq实现为Python方法),并且不应该依赖它们的原子性。 你也不应该依赖于原子变量赋值(因为这又取决于字典)。
对于一般情况来说,遵循这个建议就够了。
在某些情况下,例如实现新的锁功能或性能至关重要时,可能仍然存在一些情况。 依靠操作的原子性有效地允许您在GIL上搭载锁定,从而降低额外锁的成本。 但是,如果锁的性能如此重要,你最好首先分析热点并寻找其他加速点。(也就是说,一般来说锁的性能不会如此重要)
那么在访问或修改共享可变状态时依赖操作的原子性是否合理呢?
简短的回答:
如果这样做,你最好有一个很好的理由。
你最好做一些彻底的研究,弄清楚其中的原理。
否则,你最好使用锁。
英文原文:http://blog.qqrs.us/blog/2016/05/01/which-python-operations-are-atomic/
译者:LJ
长按下方图片识别二维码,领取免费人工智能课程