Python 2.X atomic中的`print`内置函数是什么?

时间:2017-07-20 21:14:50

标签: python multithreading python-2.7 atomic atomicity

本周我一直在探索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_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然而,我们语无伦次或愚蠢地告诉我们它是一个简单的函数,因此是一个原子操作?!

注意:我从不依赖线程的顺序或时间来使应用程序正常工作。这只是为了测试目的,坦率地说是像我这样的极客。

1 个答案:

答案 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_THREADSPy_BEGIN_ALLOW_THREADS宏有23种用途,其中一种用于file.write的实现,print依赖于此。