本周我一直在探索Python内部线程的实现。令我惊讶的是,每天我都对我不知道的事情感到惊讶;不知道我想知道什么,这是什么让我发痒。
我注意到在Python 2.7下作为多线程应用程序运行的一段代码中有些奇怪。我们都知道默认情况下,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
运行或运行'线程在函数获得运行更改之前被中断。也就是说,如果在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
然而,我们语无伦次或愚蠢地告诉我们它是一个简单的函数,因此是一个原子操作?!
注意:我从不依赖线程的顺序或时间来使应用程序正常工作。这只是为了测试目的,坦率地说是像我这样的极客。
答案 0 :(得分:2)
您的问题基于中心前提
因此,调用函数是原子的,因为它只需一条指令即可完成。
这是完全错误的。
首先,执行CALL_FUNCTION
操作码可能涉及执行额外的字节码。最明显的情况是执行的函数是用Python编写的,但即使是内置函数也可以自由调用可能用Python编写的其他代码。例如,print
调用__str__
和write
方法。
其次,即使在C代码中间,Python也可以自由发布GIL。它通常用于I / O和其他可能需要一段时间而不需要执行Python API调用的操作。仅Python 2.7 file object implementation中的FILE_BEGIN_ALLOW_THREADS
和Py_BEGIN_ALLOW_THREADS
宏有23种用途,其中一种用于file.write
的实现,print
依赖于此。