2017-07-20 33 views
0

我这个星期一直在探索在Python内部执行的线程。我很惊讶我每天都不知道自己有多少惊讶,不知道我想知道什么,这就是让我痒。Python 2.X中的print`内建函数原子吗?

我发现了一些在一块的代码,我的Python 2.7下运行的应用程序mutlithreaded奇怪。我们都知道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指令。所以,如果你有这样多线程程序(从编程的Python第四版借,略作修改):

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现在是一个函数,它如何打印传入的字符串而不是新行?在印刷字符串的末尾的endprint打印的值,它的默认设置为\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声明一样。然而,它会不相干地或愚蠢地告诉我们它是一个简单的函数,因此是一个原子操作?!

注:我从来不靠线程的应用程序的顺序或时间正常工作的。这仅仅是为了测试的目的,并坦率地说,像我这样的怪才。

+1

它**不可能**在一般情况下原子的,因为它能够处理任意大小,这必然包括比操作系统内核将同意在一个单一的系统调用来处理更大尺寸的字符串。 –

+0

https://stackoverflow.com/questions/3029816/how-do-i-get-a-thread-safe-print-in-python-2-6 –

+3

......这就是说 - 只是因为某些东西原子在Python解释器的层并不意味着它在原子层以下的层。 [抽象漏洞](https://www.joelonsoftware.com/2002/11/11/the-law-of-leaky-abstractions/) –

回答

2

你的问题是基于核心前提

因此调用函数,因为它是用一条指令完成的是原子。

这是完全错误的。

首先,执行CALL_FUNCTION操作码可涉及执行额外的字节码。最明显的例子是当执行的函数是用Python编写的,但即使是内置函数也可以自由地调用可能用Python编写的其他代码。例如,print调用__str__write方法。

其次,Python是免费的释放GIL即使在C代码中。它通常为I/O和其他需要一段时间而无需执行Python API调用的操作执行此操作。有仅在Python 2.7 file object implementation 23种用途FILE_BEGIN_ALLOW_THREADSPy_BEGIN_ALLOW_THREADS宏,包括一个在file.write实施,这print依赖。

+1

干得好!现在没有神秘:-) – direprobs

相关问题