Python併發程式設計入門

語言: CN / TW / HK

導讀:Python引入併發程式設計可以有效地提升程式的執行速度,學習並掌握併發程式設計,是高級別程式設計師的必備能力,常見的併發程式設計包括多執行緒、多程序,兩者分別應對IO密集型和CPU密集型問題。

概況

NO.1  

為什麼要引入併發程式設計

場景1:一個網路爬蟲,按順序爬取花了1小時,採用併發下載減少到20分鐘;

場景2:一個個性化作業推薦的場景,3000人同時生成個性化作業,單執行緒需要17S,而採用多程序後只需0.5S;

結論:

1、引入併發,就是為了提升程式執行速度,提升 CPU 的計算能力的利用率和程式效能;

2、併發程式可以更好地處理複雜業務,對複雜業務進行多工拆分,簡化任務排程,同步執行任務;

3、學習並掌握併發程式設計,是高級別程式設計師的必備能力

NO.2  

有哪些程式提速方法?

對於併發程式設計python有多種長期支援的方法,包括多執行緒、多程序以及各種各樣的關於生成器函式的技巧,此外還有多機器並行方式(不在本文討論範圍內)。

NO.3  

Python對併發程式設計的支援

1、多執行緒:threading,利用CPU和IO可以同時執行的原理,讓CPU和IO切換進行;

2、多程序:multiprocessing,利用多核CPU的能力,真正的並行執行任務;

3、非同步I/O:asyncio, 在單執行緒模式下利用CPU和I/O同時執行的原理,實現函式非同步任務;

4、使用Lock對資源加鎖,防止多執行緒訪問同一資源出現衝突;

5、使用Queue實現不同執行緒/程序之間的資料通訊,實現生產者-消費者模式,比如爬蟲邊爬取邊解析。

6、使用執行緒池Pool/程序池Pool,簡化執行緒/程序的任務提交、等待結束、獲取結果

如何選擇多執行緒多規劃

python的併發程式設計有三種方式:多執行緒Thread、多程序Process、多協程Coroutine(不在本次討論範圍),如何選擇合適的併發程式設計方式?

NO.1  

什麼是CPU密集型計算、IO密集型計算?

CPU密集型(CPU-bound):

CPU密集型也叫計算密集型,是指I/O在很短的時間就可以完成,CPU需要大量的計算和處理,特點是CPU佔用率相當高

例如:壓縮解壓縮、加密解密、正則表示式搜尋

IO密集型(I/O bound):

IO密集型指的是系統運作大部分的狀況是CPU在等I/O (硬碟/記憶體) 的讀/寫操作,CPU佔用率仍然較低。

例如:檔案處理程式、網路爬蟲程式、讀寫資料庫程式

NO.2  

多執行緒、多程序的對比

多程序 Process(multiprocessing)

優點:可以利用多核CPU並行運算

缺點:點用資源最多、可啟動數目比執行緒少

適用於:CPU密集型計算

多執行緒Thread(threading)

優點:相比程序,更輕量級、佔用資源少

缺點:相比程序:多執行緒只能併發執行,不能利用多CPU(GIL),相比協程:啟動數目有限制,佔用記憶體資源,有執行緒切換開銷

適用於:IO密集型計算、同時執行的任務數目要求不多

NO.3  

怎樣根據任務選擇對應技術

CPU密集型:多程序;

IO密集型:多執行緒。

全域性直譯器鎖GIL

NO.1  

Python速度慢的兩大原因

相比C/C++/JAVA,Python確實慢,在一些特殊場景下,Python比C++慢100~200倍

python慢的原因:

1、 解釋型語言 :由於python是解釋執行的,如果你將那些效能瓶頸程式碼移到一個C語言擴充套件模組中,速度也會提升的很快。如果你要運算元組,那麼使用numpy這樣的擴充套件會非常的高效;

2、 全域性直譯器鎖(GIL) :如果一個執行緒長期持有GIL的話會導致其他非CPU型執行緒一直等待。

NO.2  

GIL是什麼?

全域性直譯器鎖(英語:Global Interpreter Lock,縮寫GIL)

儘管python完全支援多執行緒程式設計,但是直譯器的C語言實現部分在完全並行執行時並不是執行緒安全的。實際上,直譯器被一個全域性直譯器鎖保護著,它確保任何時候都只有一個python執行緒執行。GIL最大的問題就是python的多執行緒程式並不能利用多核CPU的優勢(比如一個使用了多個執行緒的計算密集型程式只會在一個單CPU上面執行)

由於GIL的存在即使電腦有多核CPU單個時刻也只能使用1個。

上面這張圖,就是 GIL 在 Python 程式的工作示例。其中,Thread 1、2、3 輪流執行,每一個執行緒在開始執行時,都會鎖住 GIL,以阻止別的執行緒執行;同樣的,每一個執行緒執行完一段後,會釋放 GIL,以允許別的執行緒開始利用資源。

NO.3  

為什麼有GIL這個東西?

簡而言之:Python為了利用多核CPU,開始支援多執行緒。而解決多執行緒之間資料完整性和狀態同步的最簡單方法自然就是加鎖,於是有了GIL這把超級大鎖。

但當大家試圖去拆分和去除GIL的時候,多執行緒的問題依然還是要面對。所以簡單的說:GIL的存在更多的是歷史原因。

為了解決多執行緒之間資料完整性和狀態同步問題,Python中物件的管理,是使用引用計數器進行的,引用數為0則釋放物件,開始:執行緒A和執行緒B都引用了物件obj,obj.ref_num = 2,執行緒A和B都想撤銷對obj的引用。

NO.4  

怎樣規避GIL帶來的限制

1、多執行緒 threading 機制依然是有用的,在討論普通的GIL之前,有一點要強調的是GIL只會影響到那些嚴重依賴CPU的程式(比如計算型的)。如果你的程式大部分只會涉及到IO,比如網路互動,那麼使用多執行緒就很合適,因為它們大部分時間都在等待。實際上,你完全可以放心的建立幾千個python執行緒,現代作業系統執行這麼多執行緒沒有任何壓力,沒啥可擔心的,而對於依賴CPU的程式,你需要弄清楚執行的計算的特點。例如,優化底層演算法要比使用多執行緒執行快得多。

2、使用multiprocessing 的多程序機制實現平行計算、利用多核CPU優勢,為了應對GIL的問題,Python提供了multiprocessing。

多執行緒實戰

NO.1  

Python建立多執行緒的方法

## 1、準備一個函式
def my_func(a, b):
craw(a,b)


## 2、怎樣建立一個執行緒
import threading
t = threading.Thread(target=my_func, args=(100, 200)


## 3、啟動執行緒
t.start()


## 4、等待結束
t.join()

例1:用多執行緒完成爬蟲的10倍加速

import requests
import threading
import time


# 預先準備好的需要爬取的list
urls = []


def craw(url):
r = requests.get(url)
print(url, len(r.text))






def single_thread():
print('single_thread start')
for url in urls:
craw(url)
print('single_thread end')




def multi_thread():
print('multi_thread start')
threads = []
for url in urls:
threads.append(threading.Thread(target=craw, args=(url,)))


for thread in threads:
thread.start()


for thread in threads:
thread.join()
print('multi_thread end')




if __name__ == '__main__':
start = time.time()
single_thread()
end = time.time()
print('single_thread cost:', end - start, 'seconds')


start = time.time()
multi_thread()
end = time.time()
print('multi_thread cost:', end - start, 'seconds')

最終單執行緒使用了約3.8S,多執行緒僅用了0.37S。

NO.2  

併發的執行緒安全問題

執行緒安全指某個函式、函式庫在多執行緒環境中被呼叫時,能夠正確地處理多個執行緒之間的共享變數,使程式功能正確完成。

在多個執行緒對全域性變數進行修改時,造成得到的結果不正確,這種情況就是執行緒安全問題。

例2:多執行緒資源競爭

from threading import Thread


num = 0


def add_num():
global num
for i in range(100000):
num += 1


if __name__ == '__main__':
t1 = Thread(target=add_num)
t2 = Thread(target=add_num)
t3 = Thread(target=add_num)
t1.start()
t2.start()
t3.start()
print(num)

上述案例,我們建立了三個執行緒,每個執行緒都是將num進行十萬次+1運算,因為三個執行緒是共享全域性變數的,所以結果應該是300000,但最終結果是249422(每次不同)。

這是由於多執行緒同時操作,有可能出現下面情況:

1、在num=0時,t1取得num=0,但還沒有開始做+1運算。此時系統把t1排程為”sleeping”狀態,把t2轉換為”running”狀態,t2也獲得num=0

2、 然後t2對得到的值進行加1並賦給num,使得num=1

3、然後系統又把t2排程為”sleeping”,把t1轉為”running”。執行緒t1把它之前得到的0加1後賦值給num。

4、這樣導致雖然t1和t2都對num加1,但結果仍然是num=1

LOCK用於解決執行緒安全問題

# 用法1:try-finally模式
import threading
lock = threading.Lock()
lock.acquire()
try:
# do something
finally:
lock.release()


# 用法2:with 模式
import threading
lock = threading.Lock()
with lock:
# do something

在上述案例中,只要在迴圈內部加入lock方法,就能夠保證執行緒的安全。

執行緒池

NO.1  

體執行緒池原理

CPU 在輪換執行執行緒過程中,執行緒都經歷了什麼呢?執行緒從建立到消亡的整個過程,可能會歷經 5 種狀態,分別是新建、就緒、執行、阻塞和死亡,如圖所示

新建執行緒系統需要分配資源、終止執行緒系統需要回收資源
如果可以重用執行緒(線上程池),則可以減去新建/終止的開銷

當呼叫執行緒池execute() 方法新增一個任務時,執行緒池會做如下判斷:

1、如果有空閒執行緒,則直接執行該任務;

2、如果沒有空閒執行緒,且當前執行的執行緒數少於corePoolSize,則建立新的執行緒執行該任務;

3、如果沒有空閒執行緒,且當前的執行緒數等於corePoolSize,同時阻塞佇列未滿,則將任務入佇列,而不新增新的執行緒;

4、如果沒有空閒執行緒,且阻塞佇列已滿,同時池中的執行緒數小於maximumPoolSize ,則建立新的執行緒執行任務;

5、如果沒有空閒執行緒,且阻塞佇列已滿,同時池中的執行緒數等於maximumPoolSize ,則根據建構函式中的 handler 指定的策略來拒絕新的任務。
其中:

6、corePoolSize :執行緒池中核心執行緒數的最大值

7、maximumPoolSize :執行緒池中能擁有最多執行緒數

8、workQueue:用於快取任務的阻塞佇列

NO.2  

使用執行緒池的好處

1、提升效能:因為減去了大量新建、終止執行緒的開銷,重用了執行緒資源;

2、適用場景:適合處理突發性大量請求或需要大量執行緒完成任務、但實際任務處理時間較短

3、防禦功能:能有效避免系統因為建立執行緒過多,而導致系統負荷過大相應變慢等問題

4、程式碼優勢:使用執行緒池的語法比自己新建執行緒執行執行緒更加簡潔

NO.3  

ThreadPoolExecutor的使用語法

from concurrent.futures import ThreadPoolExecutor, as_completed


# 用法1:map函式,很簡單 注意map的結果和入參是順序對應的
with ThreadPoolExecutor() as pool:
results = pool.map(craw, urls)
for result in results:
print(result)


# 用法2:future模式,更強大 注意如果用as_completed順序是不定的
with ThreadPoolExecutor() as pool:

futures = [pool.submit(craw, url)
for url in urls ]

for future in as_completed(futures):
print(future.result())

使用多程序multiprocessing

加速程式的執行

NO.1  

具體有了多執行緒threading, 為什麼還要用多程序multiprocessing

如上圖所示,雖然有GIL的存在,但是因為IO的存在多執行緒依舊可以對程式進行加速執行。

但是如果遇到了CPU密集型計算,執行緒的切換反而成了負擔,拖慢了程式的執行速度!

而multiprocessing模組就是python為了解決GIL缺陷引入的一個模組,原理是用多程序在多CPU上並行執行!

NO.2  

具體多程序multiprocessing知識梳理  (對比多執行緒threading)

現有場景下單執行緒、多執行緒、多程序執行速度對比

現有的個性化作業推薦場景涉及不同知識點按照掌握程度(培優、補差)題目量分配,知識點對應的題型(選擇、填空、解答)題目量分配,而且每個學生的情況不同,因此初步判斷該過程是CPU密集型的問題,針對單執行緒、多執行緒、多程序進行實驗,最終結果如下:

可以看出只有當樣本量達到一定的數量時多程序的優勢才能體現出來,多程序並非在任何情況下都優於單執行緒,當樣本量超過70時在該場景下多程序開始有明顯優勢。

參考文獻

1.https://blog.csdn.net/liuxiao723846/article/details/108026782

2.http://c.biancheng.net/view/2606.html

3.https://blog.csdn.net/gua_niu123/article/details/111350343

4.https://blog.csdn.net/qq_50840738/article/details/123861602

5.https://blog.51cto.com/u_14575624/4369560

6.[螞蟻學Python]Python併發程式設計

7.《Python+Cookbook》第三版中文v3.0.0

掃描下方二維碼新增 「好未來技術」 微信官方賬號

進入好未來技術官方交流群與作者實時互動~

(若掃碼無效,可通過微訊號 TAL-111111 直接新增)

- 也許你還想看 -

Orchestrator 在好未來資料庫高可用系統中的應用

基於雙模檢測的通話錄音質檢解決方案

混合雲網絡治理一期總結,二期展望

容器化後資源與成本優化實踐

我知道你“在看”喲~