想像一下,你有一個巨大的問題要解決,而你獨自一人。你需要計算八個不同數字的平方根。你是做什麼的?你沒有太多選擇。從第一個數字開始,然後計算結果。然後,你繼續和其他人。
如果你有三個擅長數學的朋友願意幫助你呢?他們每個人都會計算兩個數字的平方根,你的工作會更容易,因為工作量在你的朋友之間平均分配。這意味著你的問題將更快解決。
好了,一切都清楚了嗎?在這些例子中,每個朋友代表CPU的核心。在第一個範例中,整個任務由你依序解決。這稱為串行計算。在第二個範例中,由於你總共使用了四個內核,因此你使用的是並行計算。並行計算涉及使用並行進程或在處理器的多個核心之間劃分的進程。
我們已經確定了什麼是並行編程,但我們如何使用它?我們之前說過,平行運算涉及在處理器的多個核心之間執行多個任務,這意味著這些任務是同時執行的。在進行並行化之前,你應該考慮幾個問題。例如,是否有其他最佳化可以加快我們的計算速度?
現在,讓我們理所當然地認為並行化是最適合的解決方案。平行計算主要有三種模式:
#完全平行。任務可以獨立運行,不需要相互通訊。
共享記憶體並行。進程(或執行緒)需要通信,因此它們共享一個全域位址空間。
訊息傳遞。進程需要在需要時共享訊息。
在本文中,我們將說明第一個模型,它也是最簡單的。
在 Python 中實現並行性的一種方法是使用multiprocessing 模組。 multiprocessing
模組可讓你建立多個進程,每個進程都有自己的 Python 解釋器。因此,Python 多進程實作了基於進程的並行。
你可能聽過其他函式庫,像是threading
,它也是Python內建的,但它們之間有著重要的差異。 multiprocessing
模組建立新進程,而threading
建立新執行緒。
你可能會問,「為什麼選擇多進程?」 多進程可以透過並行而不是按順序運行多個任務來顯著提高程式的效率。一個類似的術語是多線程,但它們是不同的。
進程是載入到記憶體中運行的程序,不與其他進程共享其記憶體。執行緒是進程中的一個執行單元。多個執行緒在一個進程中運行,並相互共享進程的記憶體空間。
Python的全域解釋器鎖定(GIL)只允許在解釋器下一次運行一個線程,這意味著如果需要Python解釋器,你將無法享受多線程的效能優勢。這就是在Python中多進程比執行緒更佔優勢的原因。多個進程可以並行運行,因為每個進程都有自己的解釋器,執行分配給它的指令。此外,作業系統將在多個進程中查看你的程序,並分別對它們進行調度,即,你的程式在總的電腦資源中佔有更大的份額。因此,當程式受到CPU限制時,多進程速度更快。在程式中有大量I/O的情況下,執行緒可能更有高效,因為大多數時候,程式都在等待I/O完成。然而,多進程通常效率更高,因為它同時運行。
以下是多進程的一些好處:
在處理高CPU密集型任務時更好地使用CPU
與執行緒相比,對子執行緒的控制更多
易於編碼
第一個優點與效能有關。由於多進程創建了新的進程,你可以透過在其他核心之間劃分任務來更好地利用CPU的運算能力。現在大多數處理器都是多核心處理器,如果你優化程式碼,可以透過並行計算節省時間。
第二個優點是多執行緒處理的替代方案。執行緒不是進程,這有其後果。如果你創建了一個線程,那麼像處理正常進程一樣終止它甚至中斷它是很危險的。由於多進程和多執行緒之間的比較不在本文的範圍內,後續我會單獨寫一篇來講講多進程和多執行緒的差異。
多進程的第三個優點是它很容易實現,因為你嘗試處理的任務適合併行程式設計。
我們終於準備好要寫一些 Python 程式碼了!
我們將從一個非常基本的範例開始,我們將使用它來說明 Python 多進程的核心面向。在這個範例中,我們將有兩個進程:
parent
經常。只有一個父進程,它可以有多個子進程。
child
進程。這是由父進程產生的。每個子進程也可以有新的子進程。
我們將使用該child
程序來執行某個函數。這樣,parent
可以繼續執行。
這是我們將用於此範例的程式碼:
from multiprocessing import Process def bubble_sort(array): check = True while check == True: check = False for i in range(0, len(array)-1): if array[i] > array[i+1]: check = True temp = array[i] array[i] = array[i+1] array[i+1] = temp print("Array sorted: ", array) if __name__ == '__main__': p = Process(target=bubble_sort, args=([1,9,4,5,2,6,8,4],)) p.start() p.join()
在這個片段中,我們定義了一個名為bubble_sort(array)
。這個函數是冒泡排序演算法的一個非常簡單的實作。如果你不知道它是什麼,請不要擔心,因為它並不重要。要知道的關鍵是它是一個可以實現某個功能的函數。
從multiprocessing
,我們導入類別Process
。此類表示將在單獨進程中運行的活動。事實上,你可以看到我們已經傳遞了一些參數:
target=bubble_sort
,意味著我們的新進程將運行該bubble_sort
函數
args=([1,9,4,52,6,8,4],)
,這是作為參數傳遞給目標函數的陣列
一旦我們建立了Process 類別的實例,我們只需要啟動該進程。這是透過編寫p.start()
完成的。此時,該進程開始。
在我們退出之前,我們需要等待子程序完成它的計算。此join()
方法等待進程終止。
在這個範例中,我們只建立了一個子進程。正如你可能猜到的,我們可以透過在Process
類別中建立更多實例來建立更多子進程。
如果我們需要建立多個進程來處理更多 CPU 密集型任務怎麼辦?我們是否總是需要明確地開始並等待終止?這裡的解決方案是使用Pool
類別。
Pool
類別允許你建立一個工作進程池,在下面的範例中,我們將研究如何使用它。這是我們的新範例:
from multiprocessing import Pool import time import math N = 5000000 def cube(x): return math.sqrt(x) if __name__ == "__main__": with Pool() as pool: result = pool.map(cube, range(10,N)) print("Program finished!")
在這個程式碼片段中,我們有一個cube(x)
函數,它只接受一個整數並傳回它的平方根。很簡單,對吧?
然後,我們建立一個Pool
類別的實例,而不指定任何屬性。預設情況下,Pool
類別為每個 CPU 核心建立一個進程。接下來,我們使用幾個參數來執行map
方法。
map
方法將cube
函數應用於我們提供的可迭代物件的每個元素—在本例中,它是從10
到N
的每個數字的清單。
這樣做的最大優點是清單上的計算是並行進行的!
套件joblib
是一組讓平行運算更容易的工具。它是一個用於多進程的通用第三方程式庫。它還提供快取和序列化功能。要安裝joblib
套件,請在終端機中使用以下命令:
pip install joblib
我們可以將先前的範例轉換為以下範例以供使用joblib
:
from joblib import Parallel, delayed def cube(x): return x**3 start_time = time.perf_counter() result = Parallel(n_jobs=3)(delayed(cube)(i) for i in range(1,1000)) finish_time = time.perf_counter() print(f"Program finished in {finish_time-start_time} seconds") print(result)
事實上,直觀地看到它的作用。 delayed()
函數是另一個函數的包裝器,用於產生函數呼叫的「延遲」版本。這意味著它在被呼叫時不會立即執行函數。
然后,我们多次调用delayed
函数,并传递不同的参数集。例如,当我们将整数1
赋予cube
函数的延迟版本时,我们不计算结果,而是分别为函数对象、位置参数和关键字参数生成元组(cube, (1,), {})
。
我们使用Parallel()
创建了引擎实例。当它像一个以元组列表作为参数的函数一样被调用时,它将实际并行执行每个元组指定的作业,并在所有作业完成后收集结果作为列表。在这里,我们创建了n_jobs=3
的Parallel()
实例,因此将有三个进程并行运行。
我们也可以直接编写元组。因此,上面的代码可以重写为:
result = Parallel(n_jobs=3)((cube, (i,), {}) for i in range(1,1000))
使用joblib
的好处是,我们可以通过简单地添加一个附加参数在多线程中运行代码:
result = Parallel(n_jobs=3, prefer="threads")(delayed(cube)(i) for i in range(1,1000))
这隐藏了并行运行函数的所有细节。我们只是使用与普通列表理解没有太大区别的语法。
创建多个进程并进行并行计算不一定比串行计算更有效。对于 CPU 密集度较低的任务,串行计算比并行计算快。因此,了解何时应该使用多进程非常重要——这取决于你正在执行的任务。
为了让你相信这一点,让我们看一个简单的例子:
from multiprocessing import Pool import time import math N = 5000000 def cube(x): return math.sqrt(x) if __name__ == "__main__": # first way, using multiprocessing start_time = time.perf_counter() with Pool() as pool: result = pool.map(cube, range(10,N)) finish_time = time.perf_counter() print("Program finished in {} seconds - using multiprocessing".format(finish_time-start_time)) print("---") # second way, serial computation start_time = time.perf_counter() result = [] for x in range(10,N): result.append(cube(x)) finish_time = time.perf_counter() print("Program finished in {} seconds".format(finish_time-start_time))
此代码段基于前面的示例。我们正在解决同样的问题,即计算N
个数的平方根,但有两种方法。第一个涉及 Python 进程的使用,而第二个不涉及。我们使用time
库中的perf_counter()
方法来测量时间性能。
在我的电脑上,我得到了这个结果:
> python code.py Program finished in 1.6385094 seconds - using multiprocessing --- Program finished in 2.7373942999999996 seconds
如你所见,相差不止一秒。所以在这种情况下,多进程更好。
让我们更改代码中的某些内容,例如N
的值。 让我们把它降低到N=10000
,看看会发生什么。
这就是我现在得到的:
> python code.py Program finished in 0.3756742 seconds - using multiprocessing --- Program finished in 0.005098400000000003 seconds
发生了什么?现在看来,多进程是一个糟糕的选择。为什么?
与解决的任务相比,在进程之间拆分计算所带来的开销太大了。你可以看到在时间性能方面有多大差异。
以上是Python多進程怎麼應用的詳細內容。更多資訊請關注PHP中文網其他相關文章!