Python中if __name__=="__main__" 語句在調用多進程Process過程中的作用分析,__name____main__
2018年2月27日 於創B515
引言
最近準備學習一下如何使用Python中的多進程。在翻看相關書籍、網上資料時發現所有代碼都含有if __name__=="__main__",在實驗的過程中發現如果在運行代碼過程中,沒有這句話Python解譯器就會報錯。雖然Python對於multiprocessing的文檔第17.2.1.1節中【1】提到必須如此使用,但是我覺得並沒有根本上解釋清楚。因此我決定從源碼來解釋我的疑惑。
# 代碼0.1錯誤碼
import multiprocessing as mpimport osdef do(): print("pid is : %s ..." % os.getpid())print("parent id is : %s ..." % os.getpid())p = mp.Process(target=do, args=())p.start()
# 代碼0.2正確代碼import multiprocessing as mpimport osdef do(): print("pid is : %s ..." % os.getpid())if __name__ == '__main__': print("parent id is : %s ..." % os.getpid()) p = mp.Process(target=do, args=()) p.start()
問題描述
問題
在運行代碼-0.1時,會出現RuntimeError,錯誤提示如下。但是運行代碼0.2時就不會,一切順利。
An attempt has been made to start a new process before the current process has finished its bootstrapping phase. This probably means that you are not using fork to start your child processes and you have forgotten to use the
proper idiom in the main module: if __name__ == '__main__': freeze_support() ... The "freeze\_support()" line can be omitted if the program is not going to be frozen to produce an executable.
問題產生的環境
環境配置
| 運行環境: |
Win10 |
| IDE |
Sublime Text3 |
簡單解釋
由於Python運行過程中,新建立進程後,進程會匯入正在啟動並執行檔案,即在運行代碼0.1的時候,代碼在運行到mp.Process時,新的進程會重新讀入改代碼,對於沒有if __name__=="__main__"保護的代碼,新進程都認為是要再次啟動並執行代碼,這是子進程又一次運行mp.Process,但是在multiprocessing.Process的源碼中是對子進程再次產生子進程是做了限制的,是不允許的,於是出現如上的錯誤提示。
詳細解釋
先談一談if__name__=="__main__"
在Python有關__main__的文檔中【2】說明“__main__”是代碼執行時的最高的命名空間(the name of the scope in which top-level code executes),當代碼被當做指令碼讀入的時候,命名空間會被命名為“__main__”,對於在指令碼運行過程中讀入的代碼命名空間都不會被命名為“__main__”。這也就是說建立的子進程是不會讀取__name__=="__main__"保護下的代碼。
再談一談multiprocessing(win32下的源碼分析)
multiprocessing根據平台不同會執行不同的代碼:在類UNIX系統下由於作業系統本身支援fork()語句,win32系統由於本身不支援fork(),因此在兩種系統下multiprocessing會運行不同的代碼,1 UNIX平台、圖2 win32平台(包含在context.py檔案中,Process的定義也是在context.py檔案中)。
圖1 對於類UNIX系統平台
圖2 對於win32系統平台
Process是一個高度依賴繼承的類———其父類是BaseProcess,3 Process類的定義。在使用過程中先初始化一個Process執行個體,然後通過Process.start()來啟動子進程。我們繼續看關於Process.start()的定義,4 BaseProcess類中的start()定義。在其中第105行,調用了self._Popen(self),該函數重定義定義於Process類。該函數按調用順序最後會跳轉到 popen_spwan_win32.py 檔案中的Popen類,5 Popen類的定義。Popen類在初始話過程中首先調用windows相關介面,產生一個管道(38行),取得對管道讀寫的控制代碼,然後產生一個命令列的字串列表——cmd,如下:
['C:\\Program Files\\Python35\\python.exe', '-B', '-c', 'from multiprocessing.spawn import spawn_main;spawn_main(pipe_handle=928, parent_pid=9292)', '--multiprocessing-fork']
圖3 Process類的定義
圖4 BaseProcess類中的start()定義
圖5 Popen類的定義
這個字串列表之後通過命令列送入系統,得到相應的子進程和子線程的控制代碼及子進程和子線程的ID號。在第65、66行是通過pickle方法將必要的資料從父進程傳輸給子進程。新進程是調用spawn.py檔案中的spawn_main函數,6 spawnmain的定義。其中第100行的steal_handle()的作用是子進程擷取父進程產生的控制代碼,用於後續通訊——使用pickle方法從父進程中讀取必要資料。而問題的出現就是在這之後出現!該代碼在第106行調用_main(),其次在第115行調用prepare(),再次運行到223行時,運行_fixup_main_from_name(),而此時該函數會運行父進程的指令碼。因此對於沒有if__name__=="__main__"保護的代碼都是要啟動並執行,而此時在第二次運行Process建立新進程的時候在第123行 if getattr(process.current_process(), '_inheriting', False): 時,由於子進程是具有_inheriting屬性,因此會激發出上述錯誤碼。
圖6 spawn_main的定義
對於代碼中產生新子進程時所用到的兩個技術我覺很有趣也很苦惱,因此決定繼續看下去。下面簡單描述一下兩個技術的概況。
—— pickle模組
首先是pickle模組。pickle模組是一個Python中特有的資料格式,與JSON等不同,是不能被其他語言識別的格式。在Python中的官方文檔【3】中比較了pickle模組與JSON的不同之處,以及介紹了pickle的使用條件。簡單摘錄如下:
pickle與JSON的區別
- JSON 是text文字格式設定而pickle是二進位流
- JSON 是可讀的而pickle是不可讀的
- JSON 可用於其他語言環境,而pickle僅僅用於Python自身
- JSON 對於Python中的一些內建(built-in)類型會失效,而pickle都是有效
|
可以被pickle模組pickle的資料
- None、True、False
- 整數、浮點數、複數
- 字串、位元組、位元組數組
- 包含可以被pickle資料的元組、列表、集合、字典
- 使用def定義的函數
- 在模組top-level定義的內建(built-in)函數和類
|
—— 管道(Pipe)
這裡理解管道是從類UNIX系統理解,因為其理解起來更方便。管道在類UNIX系統中也是一種檔案,在產生新的子進程的時候將兩個進程都關聯至同一個管道上,這樣就是雙工通訊(父子進程相互可讀可寫)。如果要實現單工通訊,就關閉相應的通道(一方寫一方讀)【4】。
參考文獻