Python 2.X 中的 `print` 内置函数是原子的吗?
Is the `print` builtin function in Python 2.X atomic?
这周我一直在探索 Python 线程的内部实现。令人惊讶的是,我每天都对自己不知道的事情感到惊讶;不知道我想知道什么,这就是让我发痒的原因。
我注意到我 运行 在 Python 2.7 下作为多线程应用程序的一段代码中有些东西 st运行ge。我们都知道 Python 2.7 默认在 100 条虚拟指令后在线程之间切换。调用一个函数就是一条虚指令,例如:
>>> from __future__ import print_function
>>> def x(): print('a')
...
>>> dis.dis(x)
1 0 LOAD_GLOBAL 0 (print)
3 LOAD_CONST 1 ('a')
6 CALL_FUNCTION 1
9 POP_TOP
10 LOAD_CONST 0 (None)
13 RETURN_VALUE
如您所见,在加载全局 print
和加载常量 a
之后,函数被调用。因此调用一个函数是原子的,因为它是用一条指令完成的。因此,在多线程程序中,函数(此处为 print
)运行 或 'running' 线程在函数更改为 运行 之前被中断。也就是说,如果在 LOAD_GLOBAL
和 LOAD_CONST
之间发生上下文切换,指令 CALL_FUNCTION
不会 运行。
请记住,在上面的代码中,我使用的是 from __future__ import print_function
,我实际上是在调用内置函数,而不是 print
语句。让我们看一下函数 x
的字节码,但这次使用 print
语句:
>>> def x(): print "a" # print stmt
...
>>> dis.dis(x)
1 0 LOAD_CONST 1 ('a')
3 PRINT_ITEM
4 PRINT_NEWLINE
5 LOAD_CONST 0 (None)
8 RETURN_VALUE
在这种情况下,很有可能在LOAD_CONST
和PRINT_ITEM
之间发生线程上下文切换,从而有效地阻止了PRINT_NEWLINE
指令的执行。所以如果你有一个像这样的多线程程序(借自Programming Python第4版并稍作修改):
def counter(myId, count):
for i in range(count):
time.sleep(1)
print ('[%s] => %s' % (myId, i)) #print (stmt) 2.X
for i in range(5):
thread.start_new_thread(counter, (i, 5))
time.sleep(6) # don't quit early so other threads don't die
根据线程的切换方式,输出可能看起来像这样,也可能不像这样:
[0] => 0
[3] => 0[1] => 0
[4] => 0
[2] => 0
...many more...
print
声明。
如果我们用内置的print
函数改变print
语句会发生什么?让我们看看:
from __future__ import print_function
def counter(myId, count):
for i in range(count):
time.sleep(1)
print('[%s] => %s' % (myId, i)) #print builtin (func)
for i in range(5):
thread.start_new_thread(counter, (i, 5))
time.sleep(6)
如果你 运行 这个脚本足够长并且多次,你会看到类似这样的东西:
[4] => 0
[3] => 0[1] => 0
[2] => 0
[0] => 0
...many more...
鉴于以上所有解释,这怎么可能? print
现在是一个函数,为什么它打印传入的字符串而不打印新行? print
在打印字符串的末尾打印 end
的值,默认设置为 \n
。本质上,对函数的调用是原子的,它到底是怎么被打断的?
让我们大吃一惊:
def counter(myId, count):
for i in range(count):
time.sleep(1)
#sys.stdout.write('[%s] => %s\n' % (myId, i))
print('[%s] => %s\n' % (myId, i), end='')
for i in range(5):
thread.start_new_thread(counter, (i, 5))
time.sleep(6)
现在总是打印新行,不再有混乱的输出:
[1] => 0
[2] => 0
[0] => 0
[4] => 0
...many more...
现在向字符串添加 \n
显然证明了 print
函数不是原子的(即使它是一个函数)并且本质上它只是作为 print
陈述。 dis.dis
然而,语无伦次或愚蠢地告诉我们这是一个简单的函数,因此是一个原子操作?!
注意:我从不依赖线程的顺序或时间来让应用程序正常工作。这仅用于测试目的,f运行kly 适合像我这样的极客。
你的问题是基于中心前提
Calling a function therefore is atomic as it's done with a single instruction.
这是完全错误的。
首先,执行 CALL_FUNCTION
操作码可能涉及执行额外的字节码。最明显的例子是执行的函数写在Python中,但即使是内置函数也可以自由调用其他可能写在Python中的代码。例如,print
调用 __str__
和 write
方法。
其次,Python即使在C代码中间也可以自由释放GIL。它通常为 I/O 和其他可能需要一段时间的操作执行此操作,而无需执行 Python API 调用。 FILE_BEGIN_ALLOW_THREADS
和 Py_BEGIN_ALLOW_THREADS
宏仅在 Python 2.7 file object implementation 中就有 23 次使用,其中包括 print
所依赖的 file.write
的实现。
这周我一直在探索 Python 线程的内部实现。令人惊讶的是,我每天都对自己不知道的事情感到惊讶;不知道我想知道什么,这就是让我发痒的原因。
我注意到我 运行 在 Python 2.7 下作为多线程应用程序的一段代码中有些东西 st运行ge。我们都知道 Python 2.7 默认在 100 条虚拟指令后在线程之间切换。调用一个函数就是一条虚指令,例如:
>>> from __future__ import print_function
>>> def x(): print('a')
...
>>> dis.dis(x)
1 0 LOAD_GLOBAL 0 (print)
3 LOAD_CONST 1 ('a')
6 CALL_FUNCTION 1
9 POP_TOP
10 LOAD_CONST 0 (None)
13 RETURN_VALUE
如您所见,在加载全局 print
和加载常量 a
之后,函数被调用。因此调用一个函数是原子的,因为它是用一条指令完成的。因此,在多线程程序中,函数(此处为 print
)运行 或 'running' 线程在函数更改为 运行 之前被中断。也就是说,如果在 LOAD_GLOBAL
和 LOAD_CONST
之间发生上下文切换,指令 CALL_FUNCTION
不会 运行。
请记住,在上面的代码中,我使用的是 from __future__ import print_function
,我实际上是在调用内置函数,而不是 print
语句。让我们看一下函数 x
的字节码,但这次使用 print
语句:
>>> def x(): print "a" # print stmt
...
>>> dis.dis(x)
1 0 LOAD_CONST 1 ('a')
3 PRINT_ITEM
4 PRINT_NEWLINE
5 LOAD_CONST 0 (None)
8 RETURN_VALUE
在这种情况下,很有可能在LOAD_CONST
和PRINT_ITEM
之间发生线程上下文切换,从而有效地阻止了PRINT_NEWLINE
指令的执行。所以如果你有一个像这样的多线程程序(借自Programming Python第4版并稍作修改):
def counter(myId, count):
for i in range(count):
time.sleep(1)
print ('[%s] => %s' % (myId, i)) #print (stmt) 2.X
for i in range(5):
thread.start_new_thread(counter, (i, 5))
time.sleep(6) # don't quit early so other threads don't die
根据线程的切换方式,输出可能看起来像这样,也可能不像这样:
[0] => 0
[3] => 0[1] => 0
[4] => 0
[2] => 0
...many more...
print
声明。
如果我们用内置的print
函数改变print
语句会发生什么?让我们看看:
from __future__ import print_function
def counter(myId, count):
for i in range(count):
time.sleep(1)
print('[%s] => %s' % (myId, i)) #print builtin (func)
for i in range(5):
thread.start_new_thread(counter, (i, 5))
time.sleep(6)
如果你 运行 这个脚本足够长并且多次,你会看到类似这样的东西:
[4] => 0
[3] => 0[1] => 0
[2] => 0
[0] => 0
...many more...
鉴于以上所有解释,这怎么可能? print
现在是一个函数,为什么它打印传入的字符串而不打印新行? print
在打印字符串的末尾打印 end
的值,默认设置为 \n
。本质上,对函数的调用是原子的,它到底是怎么被打断的?
让我们大吃一惊:
def counter(myId, count):
for i in range(count):
time.sleep(1)
#sys.stdout.write('[%s] => %s\n' % (myId, i))
print('[%s] => %s\n' % (myId, i), end='')
for i in range(5):
thread.start_new_thread(counter, (i, 5))
time.sleep(6)
现在总是打印新行,不再有混乱的输出:
[1] => 0
[2] => 0
[0] => 0
[4] => 0
...many more...
现在向字符串添加 \n
显然证明了 print
函数不是原子的(即使它是一个函数)并且本质上它只是作为 print
陈述。 dis.dis
然而,语无伦次或愚蠢地告诉我们这是一个简单的函数,因此是一个原子操作?!
注意:我从不依赖线程的顺序或时间来让应用程序正常工作。这仅用于测试目的,f运行kly 适合像我这样的极客。
你的问题是基于中心前提
Calling a function therefore is atomic as it's done with a single instruction.
这是完全错误的。
首先,执行 CALL_FUNCTION
操作码可能涉及执行额外的字节码。最明显的例子是执行的函数写在Python中,但即使是内置函数也可以自由调用其他可能写在Python中的代码。例如,print
调用 __str__
和 write
方法。
其次,Python即使在C代码中间也可以自由释放GIL。它通常为 I/O 和其他可能需要一段时间的操作执行此操作,而无需执行 Python API 调用。 FILE_BEGIN_ALLOW_THREADS
和 Py_BEGIN_ALLOW_THREADS
宏仅在 Python 2.7 file object implementation 中就有 23 次使用,其中包括 print
所依赖的 file.write
的实现。