Python 併發編程之多線程和多進程
概念
併發
在python中,併發並不是指同一時刻有多個操作(thread, task)同時進行,相反,在某個特定的時刻,它只允許又一個操作發生,只不過線程/任務之間會相互切換,直到完成。
-
Threading
-
Asyncio
對於Threading, 操作系統知道每個線程的所有信息,因此它會做主在適當的時候做線程切換。好處是代碼容易書寫,但是在執行類似與(x+=1)這種操作的時候,可能會出現資源競爭的的情況。導致數據發生紊亂。
對於Asyncio而言,主程序想要切換任務時,必須得到此任務可以被切換的通知(任務的狀態),這樣以來就可以避免上面的資源競爭的問題了。
並行
並行,指的纔是同一時刻,同時發生,Python中的multi-processing便是這個意思,可以理解爲電腦的多核的處理器,每個處理器都會並行的執行計算自己的程序,互不干擾,從而加速了運行速度。
實戰
多線程處理CPU bond任務
def cpu_bond(number):
return sum(i * i for i in range(number))
def calculate_sums(numbers):
with concurrent.futures.ThreadPoolExecutor() as executor:
executor.map(cpu_bond, numbers)
def main_multi_threading():
start_time = time.perf_counter()
numbers = [10000000 + x for x in range(20)]
calculate_sums(numbers)
end_time = time.perf_counter()
print('multil-threading:{}'.format(end_time-start_time))
執行結果
multil-threading:17.843226651
多進程處理CPU bond 任務
def cpu_bond(number):
return sum(i*i for i in range(number))
def calc_sums(numbers):
with concurrent.futures.ProcessPoolExecutor() as exec_pro:
exec_pro.map(cpu_bond, numbers)
def main_multi_processing():
start_time = time.perf_counter()
numbers = [10000000 + x for x in range(20)]
calc_sums(numbers)
end_time = time.perf_counter()
print('multil-processing:{}'.format(end_time - start_time))
執行結果
multil-processing:4.849198713
由上,我們可以得出結論,在CPU密集型(大量計算,多個程序執行…)的任務中,多進程的優勢是明顯高於多線程的,因爲多線程內部在部分線程有阻塞的時候會去切換到其他線程所產生的時間上的開銷,導致了多線程是不適合與CPU密集型的任務;
Threading處理 I/O bond任務
import concurrent.futures
import requests
import threading
import time
def download_one(url):
resp = requests.get(url)
print('Read {} from {}'.format(len(resp.content), url))
def download_all(sites):
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
executor.map(download_one, sites)
def main():
sites = [
'https://en.wikipedia.org/wiki/Portal:...',
'https://en.wikipedia.org/wiki/Portal:...',
'https://en.wikipedia.org/wiki/Portal:...',
]
start_time = time.perf_counter()
download_all(sites)
end_time = time.perf_counter()
print('Download {} sites in {} seconds'.format(len(sites), end_time - start_time))
if __name__ == '__main__':
main()
執行結果
Read 151021 from https://en.wikipedia.org/...
Read 129886 from https://en.wikipedia.org/...
Read 107637 from https://en.wikipedia.org/wiki/Portal:...
Download 15 sites in 0.19936635800002023 seconds
Asyncio 處理 I/O bond 任務
async def download_one(url):
async with aiohttp.ClientSession() as session:
async with session.get(url) as resp:
print('read {} from {}'.format(resp.content_length, url))
async def download_all(sites):
# asyncio.create_task(coro),表示對輸入的協程創建一個任務,安排它的執行,並返回此任務對象。python3.7新增的,之前的版本可以用
# asyncio.ensure_future(coro)等效代替,
tasks = [asyncio.create_task(download_one(site)) for site in sites]
# asyncio.gather(*aws, loop=None, reture_exception=False),則表示enent loop
# 中運行aws序列的所有任務。
await asyncio.gather(*tasks)
def main():
sites = [
"https://en.wikipedia.org/wiki/... ",
"https://en.wikipedia.org/wiki/... "
"https://en.wikipedia.org/wiki/... "
"..."
]
start_time = time.perf_counter()
download_all(sites)
end_time = time.perf_counter()
print('Download {} sites in {} seconds'.format(len(sites), end_time - start_time))
執行結果
Read 129886 from https://en.wi...
Download 15 sites in 2.4642311 seconds
multi-processing 處理I/O bond 任務
import requests
import concurrent.futures
def down_one(url):
resp = requests.get(url)
print('Read {} from {}'.format(len(resp.content), url))
def down_all(sites):
with concurrent.futures.ProcessPoolExecutor() as pe:
pe.map(down_one, sites)
def main_processing():
sites = [
'https://en.wikipedia.org/wiki/Portal:...',
'https://en.wikipedia.org/wiki/Portal:...',
'https://en.wikipedia.org/wiki/Portal:...',
],
start_time = time.perf_counter()
down_all(sites)
end_time = time.perf_counter()
print('Download {} sites in {} seconds'.format(len(sites), end_time - start_time))
if __name__ == '__main__':
main_processing()
執行結果
Read 129886 from https://en.wi...
Download 15 sites in 16.4642311 seconds
可以發現mutil-processing處理I/O heavy 的任務來說是非常的低效了,所以根據具體情景來選擇正確的方法,纔是一個好的coder。
總結
通過上面的大量測試,我們遇到實際的問題,該如何選擇?
- 如果是 I/O bond ,並且I/O 操作很慢,需要多任務/線程協同實現,那麼使用Asyncio更爲合適。
- 如果是I/O bond,但是I/O操作很快,只需要有限數量的任務/線程,那麼Threading即可。
- 如果是CPU bond,則需要使用multi-processing來提高程序的運行效率。
不同於多線程,Asyncio是單線程的,但是其內部event loop的機制,可以讓它併發的運行多個不同的任務,並且多線程享有更大的自主控制權。Asyncio中的任務,在運行的過程中不會被打斷,因此不會出現資源競爭的情況。另外Asyncio比多線程的運行效率要高,因爲Asyncio內部task切換的損耗,遠比線程間切換的損耗要小,並且Asyncio可以開啓的任務數量,也比多線程中的線程數量要多的多。
很多情況下,使用Asyncio需要特定的第三方庫支持,例如aiohttp,而如果I/O操作很快,並不是很密集,用多線程也能高效的解決問題。