當(dāng)你在機(jī)器上啟動(dòng)某個(gè)程序時(shí),它只是在自己的“bubble”里面運(yùn)行,這個(gè)氣泡的作用就是用來(lái)將同一時(shí)刻運(yùn)行的所有程序進(jìn)行分離。這個(gè)“bubble”也可以稱之為進(jìn)程,包含了管理該程序調(diào)用所需要的一切。
例如,這個(gè)所謂的進(jìn)程環(huán)境包括該進(jìn)程使用的內(nèi)存頁(yè),處理該進(jìn)程打開的文件,用戶和組的訪問(wèn)權(quán)限,以及它的整個(gè)命令行調(diào)用,包括給定的參數(shù)。
此信息保存在UNIX/Linux系統(tǒng)的流程文件系統(tǒng)中,該系統(tǒng)是一個(gè)虛擬文件系統(tǒng),可通過(guò)/proc目錄進(jìn)行訪問(wèn)。條目都已經(jīng)根據(jù)進(jìn)程ID排過(guò)序了,該ID是每個(gè)進(jìn)程的唯一標(biāo)識(shí)符。示例1顯示了具有進(jìn)程ID#177的任意選擇的進(jìn)程。
示例1:可用于進(jìn)程的信息
?
構(gòu)建程序代碼以及數(shù)據(jù)
程序越復(fù)雜,就越有助于將其分成較小的模塊。不僅僅源代碼是這樣,在機(jī)器上執(zhí)行的代碼也同樣適用于這條規(guī)則。該規(guī)則的典型范例就是使用子進(jìn)程并行執(zhí)行。這背后的想法就是:
單個(gè)進(jìn)程包含了可以單獨(dú)運(yùn)行的代碼段
某些代碼段可以同時(shí)運(yùn)行,因此原則上允許并行
使用現(xiàn)代處理器和操作系統(tǒng)的特性,例如可以使用處理器的所有核心,這樣就可以減少程序的總執(zhí)行時(shí)間
減少程序/代碼的復(fù)雜性,并將工作外包專門的代理
使用子進(jìn)程需要重新考慮程序的執(zhí)行方式,從線性到并行。它類似于將公司的工作視角從普通員工轉(zhuǎn)變?yōu)榻?jīng)理——你必須關(guān)注誰(shuí)在做什么,某個(gè)步驟需要多長(zhǎng)時(shí)間,以及中間結(jié)果之間的依賴關(guān)系。
這有利于將代碼分割成更小的部分,這些更小的部分可以由專門用于此任務(wù)的代理執(zhí)行。如果還沒有想清楚,試想一下數(shù)據(jù)集的構(gòu)造原理,它也是同樣的道理,這樣就可以由單個(gè)代理進(jìn)行有效的處理。但是這也引出了一些問(wèn)題:
為什么要將代碼并行化?落實(shí)到具體案例中或者在努力的過(guò)程中,思考這個(gè)問(wèn)題有意義嗎?
程序是否打算只運(yùn)行一次,還是會(huì)定期運(yùn)行在類似的數(shù)據(jù)集上?
能把算法分成幾個(gè)單獨(dú)的執(zhí)行步驟嗎?
數(shù)據(jù)是否允許并行化?如果不允許,那么數(shù)據(jù)組織將以何種方式進(jìn)行調(diào)整?
計(jì)算的中間結(jié)果是否相互依賴?
需要對(duì)硬件進(jìn)行調(diào)整嗎?
在硬件或算法中是否存在瓶頸,如何避免或者最小化這些因素的影響?
并行化的其他副作用有哪些?
可能的用例就是主進(jìn)程,以及后臺(tái)運(yùn)行的等待被激活的守護(hù)進(jìn)程(主/從)。此外,這可能是啟動(dòng)按需運(yùn)行的工作進(jìn)程的一個(gè)主要過(guò)程。在實(shí)踐中,主要的過(guò)程是一個(gè)饋線過(guò)程,它控制兩個(gè)或多個(gè)被饋送數(shù)據(jù)部分的代理,并在給定的部分進(jìn)行計(jì)算。
請(qǐng)記住,由于操作系統(tǒng)所需要的子進(jìn)程的開銷,并行操作既昂貴又耗時(shí)。與以線性方式運(yùn)行兩個(gè)或多個(gè)任務(wù)相比,在并行的情況下,根據(jù)您的用例,可以在每個(gè)子過(guò)程中節(jié)省25%到30%的時(shí)間。例如,如果在系列中執(zhí)行了兩項(xiàng)消耗5秒的任務(wù),那么總共需要10秒的時(shí)間,并且在并行化的情況下,在多核機(jī)器上平均需要8秒。這8秒中的3秒可能會(huì)在頭頂上消失,限制你的速度提高。
運(yùn)行與Python并行的函數(shù)
Python提供了四種可能的處理方式。首先可以使用multiprocessing模塊并行執(zhí)行功能。第二,進(jìn)程的替代方法是線程。從技術(shù)上講,這些都是輕量級(jí)的進(jìn)程,不在本文的范圍之內(nèi)。想了解更加詳細(xì)的內(nèi)容,可以看看Python的線程模塊。第三,可以使用os模塊的system()方法或subprocess模塊提供的方法調(diào)用外部程序,然后收集結(jié)果。
multiprocessing模塊涵蓋了一系列方法來(lái)處理并行執(zhí)行例程。這包括進(jìn)程,代理池,隊(duì)列以及管道。
清單1使用了五個(gè)代理程序池,同時(shí)處理三個(gè)值的塊。對(duì)于代理的數(shù)量和對(duì)chunksize的值都是任意選擇的,用于演示目的。根據(jù)處理器中核心的數(shù)量來(lái)調(diào)整這些值。
Pool.map()方法需要三個(gè)參數(shù) - 在數(shù)據(jù)集的每個(gè)元素上調(diào)用的函數(shù),數(shù)據(jù)集本身和chunksize。在清單1中,我們使用square函數(shù),并計(jì)算給定整數(shù)值的平方。此外,chunksize不是必須的。如果未明確設(shè)置,則默認(rèn)chunksize為1。
請(qǐng)注意,代理商的執(zhí)行訂單不能保證,但結(jié)果集的順序是正確的。它根據(jù)原始數(shù)據(jù)集的元素的順序包含平方值。
清單1:并行運(yùn)行函數(shù)
?
運(yùn)行此代碼應(yīng)該產(chǎn)生以下輸出:
注意:我們將使用Python 3作為這些例子。
使用隊(duì)列運(yùn)行多個(gè)函數(shù)
作為數(shù)據(jù)結(jié)構(gòu),隊(duì)列是非常普遍的,并且以多種方式存在。 它被組織為先進(jìn)先出(FIFO)或先進(jìn)先出(LIFO)/堆棧,以及有和沒有優(yōu)先級(jí)(優(yōu)先級(jí)隊(duì)列)。 數(shù)據(jù)結(jié)構(gòu)被實(shí)現(xiàn)為具有固定數(shù)量條目的數(shù)組,或作為包含可變數(shù)量的單個(gè)元素的列表。
在列表2.1-2.7中,我們使用FIFO隊(duì)列。 它被實(shí)現(xiàn)為已經(jīng)由來(lái)自multiprocessing模塊的相應(yīng)類提供的列表。此外,time模塊被加載并用于模擬工作負(fù)載。
清單2.1:要使用的模塊
接下來(lái),定義一個(gè)worker函數(shù)(清單2.2)。 該函數(shù)實(shí)際上代表代理,需要三個(gè)參數(shù)。進(jìn)程名稱指示它是哪個(gè)進(jìn)程,tasks和results都指向相應(yīng)的隊(duì)列。
在工作函數(shù)里面是一個(gè)while循環(huán)。tasks和results都是在主程序中定義的隊(duì)列。tasks.get()從要處理的任務(wù)隊(duì)列中返回當(dāng)前任務(wù)。小于0的任務(wù)值退出while循環(huán),返回值為-1。任何其他任務(wù)值都將執(zhí)行一個(gè)計(jì)算(平方),并返回此值。將值返回到主程序?qū)崿F(xiàn)為result.put()。這將在results隊(duì)列的末尾添加計(jì)算值。
清單2.2:worker函數(shù)
下一步是主循環(huán)(參見清單2.3)。首先,定義了進(jìn)程間通信(IPC)的經(jīng)理。接下來(lái),添加兩個(gè)隊(duì)列,一個(gè)保留任務(wù),另一個(gè)用于結(jié)果。
?
清單2.3:IPC和隊(duì)列
完成此設(shè)置后,我們定義一個(gè)具有四個(gè)工作進(jìn)程(代理)的進(jìn)程池。我們使用類multiprocessing.Pool(),并創(chuàng)建一個(gè)它的實(shí)例。 接下來(lái),我們定義一個(gè)空的進(jìn)程列表(見清單2.4)。
?
清單2.4:定義一個(gè)進(jìn)程池
?
作為以下步驟,我們啟動(dòng)了四個(gè)工作進(jìn)程(代理)。 為了簡(jiǎn)單起見,它們被命名為“P0”到“P3”。使用multiprocessing.Pool()完成創(chuàng)建四個(gè)工作進(jìn)程。這將它們中的每一個(gè)連接到worker功能以及任務(wù)和結(jié)果隊(duì)列。 最后,我們?cè)谶M(jìn)程列表的末尾添加新初始化的進(jìn)程,并使用new_process.start()啟動(dòng)新進(jìn)程(參見清單2.5)。
清單2.5:準(zhǔn)備worker進(jìn)程
?
我們的工作進(jìn)程正在等待工作。我們定義一個(gè)任務(wù)列表,在我們的例子中是任意選擇的整數(shù)。這些值將使用tasks.put()添加到任務(wù)列表中。每個(gè)工作進(jìn)程等待任務(wù),并從任務(wù)列表中選擇下一個(gè)可用任務(wù)。 這由隊(duì)列本身處理(見清單2.6)。
清單2.6:準(zhǔn)備任務(wù)隊(duì)列
?
過(guò)了一會(huì)兒,我們希望我們的代理完成。 每個(gè)工作進(jìn)程對(duì)值為-1的任務(wù)做出反應(yīng)。 它將此值解釋為終止信號(hào),此后死亡。 這就是為什么我們?cè)谌蝿?wù)隊(duì)列中放置盡可能多的-1,因?yàn)槲覀冇羞M(jìn)程運(yùn)行。 在死機(jī)之前,終止的進(jìn)程會(huì)在結(jié)果隊(duì)列中放置-1。 這意味著是代理正在終止的主循環(huán)的確認(rèn)信號(hào)。
在主循環(huán)中,我們從該隊(duì)列讀取,并計(jì)數(shù)-1。 一旦我們計(jì)算了我們有過(guò)程的終止確認(rèn)數(shù)量,主循環(huán)就會(huì)退出。 否則,我們從隊(duì)列中輸出計(jì)算結(jié)果。
清單2.7:結(jié)果的終止和輸出
?
示例2顯示了Python程序的輸出。 運(yùn)行程序不止一次,您可能會(huì)注意到,工作進(jìn)程啟動(dòng)的順序與從隊(duì)列中選擇任務(wù)的進(jìn)程本身不可預(yù)測(cè)。 但是,一旦完成結(jié)果隊(duì)列的元素的順序與任務(wù)隊(duì)列的元素的順序相匹配。
示例2
注意:如前所述,由于執(zhí)行順序不可預(yù)測(cè),您的輸出可能與上面顯示的輸出不一致。
?
使用os.system()方法
system()方法是os模塊的一部分,它允許在與Python程序的單獨(dú)進(jìn)程中執(zhí)行外部命令行程序。system()方法是一個(gè)阻塞調(diào)用,你必須等到調(diào)用完成并返回。 作為UNIX / Linux拜物教徒,您知道可以在后臺(tái)運(yùn)行命令,并將計(jì)算結(jié)果寫入重定向到這樣的文件的輸出流(參見示例3):
示例3:帶有輸出重定向的命令
在Python程序中,您只需簡(jiǎn)單地封裝此調(diào)用,如下所示:
清單3:使用os模塊進(jìn)行簡(jiǎn)單的系統(tǒng)調(diào)用
此系統(tǒng)調(diào)用創(chuàng)建一個(gè)與當(dāng)前Python程序并行運(yùn)行的進(jìn)程。 獲取結(jié)果可能會(huì)變得有點(diǎn)棘手,因?yàn)檫@個(gè)調(diào)用可能會(huì)在你的Python程序結(jié)束后終止 - 你永遠(yuǎn)都不會(huì)知道。
使用這種方法比我描述的先前方法要貴得多。 首先,開銷要大得多(進(jìn)程切換),其次,它將數(shù)據(jù)寫入物理內(nèi)存,比如一個(gè)需要更長(zhǎng)時(shí)間的磁盤。 雖然這是一個(gè)更好的選擇,你的內(nèi)存有限(像RAM),而是可以將大量輸出數(shù)據(jù)寫入固態(tài)磁盤。
使用子進(jìn)程模塊
該模塊旨在替換os.system()和os.spawn()調(diào)用。子過(guò)程的想法是簡(jiǎn)化產(chǎn)卵過(guò)程,通過(guò)管道和信號(hào)與他們進(jìn)行通信,并收集他們生成的輸出包括錯(cuò)誤消息。
從Python 3.5開始,子進(jìn)程包含方法subprocess.run()來(lái)啟動(dòng)一個(gè)外部命令,它是底層subprocess.Popen()類的包裝器。 作為示例,我們啟動(dòng)UNIX/Linux命令df -h,以查找機(jī)器的/ home分區(qū)上仍然有多少磁盤空間。在Python程序中,您可以執(zhí)行如下所示的調(diào)用(清單4)。
清單4:運(yùn)行外部命令的基本示例
?
這是基本的調(diào)用,非常類似于在終端中執(zhí)行的命令df -h / home。請(qǐng)注意,參數(shù)被分隔為列表而不是單個(gè)字符串。輸出將與示例4相似。與此模塊的官方Python文檔相比,除了調(diào)用的返回值之外,它將調(diào)用結(jié)果輸出到stdout。
示例4顯示了我們的呼叫的輸出。輸出的最后一行顯示命令的成功執(zhí)行。調(diào)用subprocess.run()返回一個(gè)類CompletedProcess的實(shí)例,它有兩個(gè)名為args(命令行參數(shù))的屬性和returncode(命令的返回值)。
示例4:運(yùn)行清單4中的Python腳本
?
要抑制輸出到stdout,并捕獲輸出和返回值進(jìn)行進(jìn)一步的評(píng)估,subprocess.run()的調(diào)用必須稍作修改。沒有進(jìn)一步修改,subprocess.run()將執(zhí)行的命令的輸出發(fā)送到stdout,這是底層Python進(jìn)程的輸出通道。 要獲取輸出,我們必須更改此值,并將輸出通道設(shè)置為預(yù)定義值subprocess.PIPE。清單5顯示了如何做到這一點(diǎn)。
清單5:抓取管道中的輸出
?
如前所述,subprocess.run()返回一個(gè)類CompletedProcess的實(shí)例。在清單5中,這個(gè)實(shí)例是一個(gè)簡(jiǎn)單命名為output的變量。該命令的返回碼保存在屬性output.returncode中,打印到stdout的輸出可以在屬性output.stdout中找到。 請(qǐng)注意,這不包括處理錯(cuò)誤消息,因?yàn)槲覀儧]有更改輸出渠道。
結(jié)論
由于現(xiàn)在的硬件已經(jīng)很厲害了,因此也給并行處理提供了絕佳的機(jī)會(huì)。Python也使得用戶即使在非常復(fù)雜的級(jí)別,也可以訪問(wèn)這些方法。正如在multiprocessing和subprocess模塊之前看到的那樣,可以讓你很輕松的對(duì)該主題有很深入的了解。
評(píng)論