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_GLOBALLOAD_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_CONSTPRINT_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_THREADSPy_BEGIN_ALLOW_THREADS 宏仅在 Python 2.7 file object implementation 中就有 23 次使用,其中包括 print 所依赖的 file.write 的实现。