2018年2月27日 於創B515html
引言
最近準備學習一下如何使用Python中的多進程。在翻看相關書籍、網上資料時發現全部代碼都含有if __name__=="__main__",在實驗的過程當中發現若是在運行代碼過程當中,沒有這句話Python解釋器就會報錯。雖然Python對於multiprocessing的文檔第17.2.1.1節中【1】提到必須如此使用,可是我以爲並無根本上解釋清楚。所以我決定從源碼來解釋個人疑惑。python
# 代碼0.1錯誤代碼
import multiprocessing as mp import os def 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 mp import os def 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時就不會,一切順利。bootstrap
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.
問題產生的環境windows
運行環境: | 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__"保護下的代碼。app
再談一談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,以下:ui
['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號。在第6五、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的使用條件。簡單摘錄以下:
|
|
—— 管道(Pipe)
這裏理解管道是從類UNIX系統理解,由於其理解起來更方便。管道在類UNIX系統中也是一種文件,在生成新的子進程的時候將兩個進程都關聯至同一個管道上,這樣就是雙工通訊(父子進程相互可讀可寫)。若是要實現單工通訊,就關閉相應的通道(一方寫一方讀)【4】。
參考文獻