Python平行計算專題
最近工作中經常涉及到Python平行計算的內容,所以今天做一期專題的知識整理。本文將涉及以下三塊內容:1. 多核/多執行緒/多程序的概念區分,2. Python多執行緒,多程序的使用方式,3. Python程序池的管理方案
多核/多執行緒/多程序
一般而言,計算機可以並行執行的執行緒數量就是CPU的物理核數,因為CPU只能看到執行緒(執行緒是CPU排程分配的最小單位)。然而,由於超執行緒技術的存在,現在的CPU可以在每個核心上執行更多的執行緒,因此,現代計算機可以並行執行的執行緒數量往往是CPU物理核數的兩倍或更多
程序是作業系統資源分配(記憶體,顯示卡,磁碟等)的最小單位,執行緒是執行排程(即CPU資源排程)的最小單位(CPU看到的都是執行緒而不是程序)。一個程序可以有一個或多個執行緒,執行緒之間共享程序的資源。如果計算機有多個核心,且計算機中的總的執行緒數量小於邏輯核數,那執行緒就可以並行執行在不同的核心中。如果是單核多執行緒,那多執行緒之間就不是並行的關係,它們只是通過分時的方式,輪流使用單個核心的計算資源,營造出一種“併發”執行的假象。由於執行緒切換需要耗費額外的資源,因此如果多執行緒協同處理的是同一個計算任務,那麼該任務的完成速度是不如單執行緒從頭算到尾的
針對計算密集型的任務,我們使用與邏輯核心數相同的執行緒數就可以最大化地利用計算資源(執行緒數少了會引起核心資源的閒置,執行緒數多了則會消耗不必要的資源在分時切換上)。如果是IO密集型任務,我們就需要建立更多的執行緒,因為當一個執行緒已經算好結果,在等待IO寫入時,我們就可以讓另一個執行緒去使用此時空閒的核心資源,在這個場景下執行緒間切換的代價是小於核心資源閒置的代價的
在上一段中,我們討論的一直都是執行緒,那麼什麼時候我們應該使用更多的程序呢?回顧之前提到過的程序的特點:
程序是作業系統資源分配的最小單位
因此,是否使用多程序,這取決於你是否想要,並且是否能夠發揮某一系統資源IO效能的最大值。舉個例子:比如你想要儘可能快地往磁碟中寫入資料,而此時一個程序並不能佔滿磁碟IO的頻寬,那麼我們就可以使用多程序,併發地往磁碟中寫入內容,進而最大化地發揮磁碟的讀寫效能
一般而言,只要CPU 有多個邏輯核心,那麼多個執行緒就能夠在不同的核心上併發執行(即使此時只有一個程序)。但對 Python 來說,無論是單核還是多核,一個程序同時只能有一個執行緒在執行。為什麼會出現這種情況呢?因為Python 在設計時採用了一種叫 GIL 的機制。GIL 的全稱為 Global Interpreter Lock (全域性直譯器鎖),它出於安全考慮,限制了每個Python程序下的執行緒,使其只有拿到GIL後,才能使用核心資源繼續工作。因此,Python的多執行緒一般只用來實現分時的“併發”,要想充分發揮硬體效能,還是需要使用多個程序
Python多執行緒和多程序的使用方法
多執行緒
我們先來看Python多執行緒的一種最簡單的使用方式:
import time
from threading import Thread
def foo(content, times):
for i in range(times):
timestamp = time.strftime('%H:%M:%S',time.localtime(time.time()))
print(f"{content} {i+1} {timestamp}")
time.sleep(1)
thread_names = ('alpha', 'bata')
repeat_times = 3
for name in thread_names:
th = Thread(target=foo, args=(name, repeat_times))
th.start()
time.sleep(0.1)
輸出:
alpha 1 21:57:58
bata 1 21:57:58
alpha 2 21:57:59
bata 2 21:57:59
alpha 3 21:58:00
bata 3 21:58:00
Python通過標準庫threading提供對執行緒的支援,其常用方法包括:
threading.currentThread()
——返回當前的執行緒變數threading.enumerate()
——返回一個包含正在執行的執行緒的list
以及Thread物件的常用方法:
start()
:啟動執行緒活動join()
:阻塞當前的程式,直至等待時間結束或其Thread物件執行終止。該方法可用於計量多執行緒程式用時isAlive()
:返回執行緒是否活動的getName()
返回執行緒名,setName()
設定執行緒名
當然,Python中的Thread不僅僅可以通過函式的方式使用,還可以以面向物件的方式使用,參考文件說明:
The
Thread
class represents an activity that is run in a separate thread of control. There are two ways to specify the activity: by passing a callable object to the constructor, or by overriding therun()
method in a subclass. No other methods (except for the constructor) should be overridden in a subclass. In other words, only override the__init__()
andrun()
methods of this class.
當我們以面向物件的方式使用Thread時,我們只需要把需要並行的部分實現至類的run()
方法中,然後在外部呼叫執行緒物件的start()
方法即可:
import time
import threading
class MyThread(threading.Thread):
def __init__(self, name, repeat_times):
super(MyThread, self).__init__()
self.content = name
self.times = repeat_times
def run(self):
for i in range(self.times):
timestamp = time.strftime('%H:%M:%S',time.localtime(time.time()))
print(f"{self.content} {i+1} {timestamp}")
time.sleep(1)
thread_names = ('alpha', 'bata')
repeat_times = 3
for name in thread_names:
th = MyThread(name, repeat_times)
th.start()
time.sleep(0.1)
該程式的輸出與之前的多執行緒函式一致
多程序
Python多程序可以使用subprocess模組,參考:multiprocessing — Process-based parallelism - Python Docs
from multiprocessing import Process
import os
# 子程序要執行的程式碼
def run_proc(name):
print('Run child process %s (%s)...' % (name, os.getpid()))
if __name__=='__main__':
print('Parent process %s.' % os.getpid())
p = Process(target=run_proc, args=('test',))
print('Child process will start.')
p.start()
p.join()
print('Child process end.')
程序池
Python程序池的使用可參考:Python使用程序池管理程序
from multiprocessing import Pool
import time
import os
def action(name='http://c.biancheng.net'):
print(name,' --當前程序:',os.getpid())
time.sleep(3)
if __name__ == '__main__':
#建立包含 4 條程序的程序池
pool = Pool(processes=4)
# 將action分3次提交給程序池
pool.apply_async(action)
pool.apply_async(action, args=('http://c.biancheng.net/python/', ))
pool.apply_async(action, args=('http://c.biancheng.net/java/', ))
pool.apply_async(action, kwds={'name': 'http://c.biancheng.net/shell/'})
pool.close()
pool.join()
參考: