Python变量赋值是原子操作吗?
假设我在使用一个 signal
处理器来处理一个定时器。
def _aHandler(signum, _):
global SomeGlobalVariable
SomeGlobalVariable=True
我可以在不担心的情况下设置 SomeGlobalVariable
吗?也就是说,在一个不太可能发生的情况下,当我正在设置 SomeGlobalVariable
(也就是 Python 虚拟机正在执行字节码来设置这个变量)时,信号处理器中的赋值会不会导致什么问题?(也就是 元稳定 状态)
更新:我特别关心的是在处理器外部进行“复合赋值”的情况。
(也许我想得太“底层”了,而这些在 Python 中都是自动处理的……因为我来自嵌入式系统的背景,所以我时不时会有这样的想法)
4 个回答
谷歌的风格指南不建议这样做
我并不是说谷歌的风格指南就是绝对正确的,但在“线程”部分的解释中有一些见解(我标记的部分):
不要依赖内置类型的原子性。
虽然Python的内置数据类型,比如字典,看起来有原子操作,但在某些特殊情况下,它们并不是原子操作(例如,如果
__hash__
或__eq__
是作为Python方法实现的),所以不应该依赖它们的原子性。你也不应该依赖原子变量赋值(因为这又依赖于字典)。使用
Queue
模块的队列数据类型作为线程间传递数据的首选方式。否则,使用线程模块及其锁机制。了解条件变量的正确使用,这样你就可以使用threading.Condition
,而不是使用更低级别的锁。
所以我的理解是,在Python中,所有东西都像字典一样,当你在后台执行a = b
时,实际上是在做globals['a'] = b
,这很糟糕,因为字典不一定是线程安全的。
对于单个变量来说,Queue
并不是理想的选择,因为我们只想让它保存一个元素,而我在标准库中找不到一个完美的现成容器,能够自动同步.set()
方法。所以目前我只是这样做:
import threading
myvar = 0
myvar_lock = threading.Lock()
with myvar_lock:
myvar = 1
with myvar_lock:
myvar = 2
有趣的是,Martelli似乎并不在意谷歌风格指南的建议 :-)(他在谷歌工作)
我想知道CPython的全局解释器锁(GIL)是否对这个问题有影响:CPython中的全局解释器锁(GIL)是什么?
这个讨论也提到CPython的字典是线程安全的,包括以下的术语引用,明确提到这一点:https://docs.python.org/3/glossary.html#term-global-interpreter-lock
这简化了CPython的实现,使得对象模型(包括关键的内置类型如字典)在并发访问时隐式安全。
你可以试试 dis
来查看底层的字节码。
import dis
def foo():
a = 1
dis.dis(foo)
这会生成以下字节码:
# a = 1
5 0 LOAD_CONST 1 (1)
2 STORE_FAST 0 (a)
所以,赋值操作只生成一条 Python 字节码(指令 2),在 CPython 中是原子的,因为它一次只执行一条字节码。
而对于加一操作 a += 1
:
def foo():
a = 1
a += 1
这会生成以下字节码:
# a+=1
6 4 LOAD_FAST 0 (a)
6 LOAD_CONST 1 (1)
8 INPLACE_ADD
10 STORE_FAST 0 (a)
+=
对应于四条指令,这就不是原子的了。
给简单变量赋值是“原子的”,也就是说它是线程安全的(像+=
这样的复合赋值,或者给对象的属性或元素赋值就不一定安全了,但你提到的例子是给一个简单的、虽然是全局的变量赋值,所以是安全的)。