2017-07-20 34 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

相關問題