1. 程式人生 > >python進階(9)多執行緒

python進階(9)多執行緒

# 什麼是執行緒? 執行緒也叫`輕量級程序`,是作業系統能夠進行`運算排程`的`最小`單位,它被包涵在程序之中,是程序中的實際運作單位。執行緒自己不擁有`系統資源`,只擁有一點兒在執行中必不可少的資源,但它可與同屬一個程序的其他執行緒共享程序所擁有的全部資源。一個執行緒可以建立和撤銷另一個執行緒,同一個程序中的多個執行緒之間可以`併發`執行   # 為什麼要使用多執行緒? 執行緒在程式中是`獨立的`、`併發的`執行流。與分隔的程序相比,程序中執行緒之間的隔離程度要小,它們共享記憶體、檔案控制代碼 和其他程序應有的狀態。 因為執行緒的劃分尺度小於程序,使得多執行緒程式的`併發性高`。程序在執行過程之中擁有獨立的記憶體單元,而多個執行緒`共享` 記憶體,從而極大的提升了程式的`執行效率`。 執行緒比程序具有更高的效能,這是由於同一個程序中的執行緒都有共性,多個執行緒共享一個程序的虛擬空間。執行緒的共享環境包括程序程式碼段、程序的共有資料等,利用這些共享的資料,執行緒之間很容易實現通訊。 作業系統在建立程序時,必須為程序分配獨立的記憶體空間,並分配大量的相關資源,但建立執行緒則簡單得多。因此,使用多執行緒來實現`併發`比使用多程序的效能高得要多。   # 多執行緒優點 程序之間不能共享記憶體,但執行緒之間共享記憶體非常容易。作業系統在建立程序時,需要為該程序重新分配系統資源,但建立執行緒的代價則小得多。因此使用多執行緒來實現`多工併發執行`比使用多程序的效率高 python語言內建了多執行緒功能支援,而不是單純地作為底層作業系統的排程方式,從而簡化了python的多執行緒程式設計。   # 單執行緒執行 ``` import time def hello(): print("你好,世界") time.sleep(1) if __name__ == "__main__": for i in range(5): hello() ``` 執行結果 ![](https://img2020.cnblogs.com/blog/1868812/202102/1868812-20210226172646967-1677477320.gif)   # 多執行緒執行 ``` import threading import time def saySorry(): print("你好,世界") time.sleep(1) if __name__ == "__main__": for i in range(5): t = threading.Thread(target=saySorry) # 建立執行緒物件,此時還未啟動子執行緒 t.start() # 啟動執行緒,即讓執行緒開始執行 ``` 執行結果 ![](https://img2020.cnblogs.com/blog/1868812/202102/1868812-20210226173249263-1566477382.gif)   # 執行速度對比 - 可以明顯看出使用了多執行緒併發的操作,花費時間要短 - 當呼叫start()時,才會真正的建立執行緒,並且開始執行   # 函式式建立多執行緒 python中多執行緒使用`threading`模組,threading模組呼叫Thread類 ``` self, group=None, target=None, name=None, args=(), kwargs=None, *, daemon=None ``` - group:預設為None;預留給將來擴充套件ThreadGroup時使用類實現。不常用,可以忽略 - target:代表要執行的函式名,不是函式 - name:執行緒名,預設情況下的格式是"Thread-N",其中N是一個小的十進位制數 - args:函式的引數,以元組的形式表示 - kwargs:關鍵字引數字典 **小例子** ``` import threading from time import sleep from datetime import datetime def write(name): for i in range(3): print("{}正在寫字{}".format(name, i)) sleep(1) def draw(name): for i in range(3): print("{}正在畫畫{}".format(name, i)) sleep(1) if __name__ == '__main__': print(f'---開始---:{datetime.now()}') t1 = threading.Thread(target=write, args=('Jack', )) t2 = threading.Thread(target=draw, args=('Tom', )) t1.start() t2.start() print(f'---結束---:{datetime.now()}') ```   # 檢視執行緒數量`threading.enumerate()` ``` import threading from datetime import datetime from time import sleep def write(): for i in range(3): print(f"正在寫字...{i}") sleep(1) def draw(): for i in range(3): print(f"正在畫畫...{i}") sleep(1) if __name__ == '__main__': print(f'---開始---:{datetime.now()}') t1 = threading.Thread(target=write) t2 = threading.Thread(target=draw) t1.start() t2.start() while True: length = len(threading.enumerate()) print(f'當前執行的執行緒數為:{length}') if length <= 1: break sleep(0.5) ``` **結果** ![](https://img2020.cnblogs.com/blog/1868812/202102/1868812-20210227085628823-234329214.gif) 最開始列印執行緒數為3個,一個主執行緒+2個子執行緒t1,t2 最後列印執行緒數為1個,是因為子執行緒都結束了,就剩主執行緒了   # 自定義執行緒 繼承`threading.Thread`來定義執行緒類,其本質是重構`Thread`類中的`run`方法 為什麼執行run方法,就會啟動執行緒呢?之前寫函式時,呼叫的是`start()`方法 因為run方法裡預設執行了`start()`方法 ``` import threading from time import sleep class MyThread(threading.Thread): def run(self): for i in range(5): sleep(1) msg = "I'm " + self.name + ' @ ' + str(i) # name屬性中儲存的是當前執行緒的名字 print(msg) if __name__ == '__main__': t = MyThread() t.start() ``` 結果 ![](https://img2020.cnblogs.com/blog/1868812/202102/1868812-20210227093746045-447499911.gif)   # 守護執行緒 ``` ''' 這裡使用setDaemon(True)把所有的子執行緒都變成了主執行緒的守護執行緒, 因此當主執行緒結束後,子執行緒也會隨之結束,所以當主執行緒結束後,整個程式就退出了。 所謂’執行緒守護’,就是主執行緒不管該執行緒的執行情況,只要是其他子執行緒結束且主執行緒執行完畢,主執行緒都會關閉。也就是說:主執行緒不等待該守護執行緒的執行完再去關閉。 ''' import threading import time def run(n): print('task', n) time.sleep(1) print('3s') time.sleep(1) print('2s') time.sleep(1) print('1s') if __name__ == '__main__': t = threading.Thread(target=run, args=('t1',)) t.setDaemon(True) t.start() print('end') ``` 結果 ``` task t1 end ``` 通過執行結果可以看出,設定守護執行緒之後,當主執行緒結束時,子執行緒也將立即結束,不再執行 # 主執行緒等待子執行緒結束(join) 為了讓守護執行緒執行結束之後,主執行緒再結束,我們可以使用`join`方法,讓主執行緒等待子執行緒執行 ``` import threading import time def run(n): print('task', n) time.sleep(1) print('3s') time.sleep(1) print('2s') time.sleep(1) print('1s') if __name__ == '__main__': t = threading.Thread(target=run, args=('t1',)) t.setDaemon(True) # 把子執行緒設定為守護執行緒,必須在start()之前設定 t.start() t.join() # 設定主執行緒等待子執行緒結束 print('end') ``` 結果 ``` task t1 3s 2s 1s end ```   # 執行緒共享變數 ``` ''' 多執行緒共享全域性變數 執行緒時程序的執行單元,程序時系統分配資源的最小執行單位,所以在同一個程序中的多執行緒是共享資源的 ''' import threading import time g_num = 0 def work1(num): global g_num for i in range(num): g_num += 1 print("----in work1, g_num is %d---"%g_num) def work2(num): global g_num for i in range(num): g_num += 1 print("----in work2, g_num is %d---"%g_num) print("---執行緒建立之前g_num is %d---"%g_num) t1 = threading.Thread(target=work1, args=(1000000,)) t1.start() t2 = threading.Thread(target=work2, args=(1000000,)) t2.start() while len(threading.enumerate()) != 1: time.sleep(1) print("2個執行緒對同一個全域性變數操作之後的最終結果是:%s" % g_num) ``` 結果 ``` ---執行緒建立之前g_num is 0--- ----in work2, g_num is 1451293--- ----in work1, g_num is 1428085--- 2個執行緒對同一個全域性變數操作之後的最終結果是:1428085 ``` 先來看結果,為什麼不是200000呢? 原因是多執行緒共用同一個變數,可能會出現資源競爭的問題,導致資料不準確,那有什麼解決辦法嗎?下面介紹互斥鎖   # 互斥鎖 由於執行緒之間是進行隨機排程,並且每個執行緒可能只執行n條執行之後,當多個執行緒同時修改同一條資料時可能會出現`髒資料`,所以出現了`執行緒鎖`,即同一時刻允許一個執行緒執行操作。執行緒鎖用於鎖定資源,可以定義多個鎖,像下面的程式碼,當需要獨佔 某一個資源時,任何一個鎖都可以鎖定這個資源,就好比你用不同的鎖都可以把這個相同的門鎖住一樣。   由於執行緒之間是進行`隨機排程`的,如果有多個執行緒同時操作一個物件,如果沒有很好地保護該物件,會造成程式結果的不可預期,我們因此也稱為`執行緒不安全`。   為了防止上面情況的發生,就出現了`互斥鎖(Lock)` ``` import threading import time g_num = 0 # 建立一個互斥鎖 # 預設是未上鎖的狀態 lock = threading.Lock() def test1(num): global g_num for i in range(num): lock.acquire() # 上鎖 g_num += 1 lock.release() # 解鎖 print("---test1---g_num=%d"%g_num) def test2(num): global g_num for i in range(num): lock.acquire() # 上鎖 g_num += 1 lock.release() # 解鎖 print("---test2---g_num=%d"%g_num) # 建立2個執行緒,讓他們各自對g_num加1000000次 p1 = threading.Thread(target=test1, args=(1000000,)) p1.start() p2 = threading.Thread(target=test2, args=(1000000,)) p2.start() # 等待計算完成 while len(threading.enumerate()) != 1: time.sleep(1) print("2個執行緒對同一個全域性變數操作之後的最終結果是:%s" % g_num) ``` 結果 ``` ---test2---g_num=1961182 ---test1---g_num=2000000 2個執行緒對同一個全域性變數操作之後的最終結果是:2000000 ```   ## 上鎖解鎖過程 當一個執行緒呼叫鎖的`acquire()`方法獲得鎖時,鎖就進入`locked`狀態。 每次只有一個執行緒可以獲得鎖。如果此時另一個執行緒試圖獲得這個鎖,該執行緒就會變為`blocked`狀態,稱為“阻塞”,直到擁有鎖的執行緒呼叫鎖的`release()`方法釋放鎖之後,鎖進入`unlocked`狀態。 執行緒排程程式從處於同步阻塞狀態的執行緒中選擇一個來獲得鎖,並使得該執行緒進入執行(running)狀態。   ## 鎖的好處 - 確保了某段關鍵程式碼只能由一個執行緒從頭到尾完整地執行   ## 鎖的壞處 - 阻止了多執行緒`併發`執行,包含鎖的某段程式碼實際上只能以`單執行緒模式`執行,效率就大大地下降了 - 由於可以存在多個鎖,不同的執行緒持有不同的鎖,並試圖獲取對方持有的鎖時,可能會造成`死鎖`   # GIL全域性直譯器 在非python環境中,單核情況下,同時只能有一個任務執行。多核時可以支援多個執行緒`同時執行`。但是在python中,無論有多少個核同時只能執行一個執行緒。究其原因,這就是由於`GIL`的存在導致的。   GIL的全程是`全域性直譯器`,來源是python設計之初的考慮,為了資料安全所做的決定。某個執行緒想要執行,必須先拿到GIL,我們可以把GIL看做是“`通行證`”,並且在一個python程序之中,GIL只有一個。拿不到通行證的執行緒,就不允許進入CPU執行。GIL只在`cpython`中才有,因為cpython呼叫的是`c語言的原生執行緒`,所以他不能直接操作cpu,而只能利用GIL保證`同一時間`只能有一個執行緒拿到資料。而在`pypy`和`jpython`中是沒有GIL的   python在使用多執行緒的時候,呼叫的是`c語言的原生過程`。   # python針對不同型別的程式碼執行效率也是不同的 1. `CPU密集型程式碼`(各種迴圈處理、計算等),在這種情況下,由於計算工作多,ticks技術很快就會達到閥值,然後出發GIL的 釋放與再競爭(多個執行緒來回切換當然是需要消耗資源的),所以python下的多執行緒對CPU密集型程式碼並不友好。   2. `IO密集型程式碼`(檔案處理、網路爬蟲等設計檔案讀寫操作),多執行緒能夠有效提升效率(單執行緒下有IO操作會進行IO等待, 造成不必要的時間浪費,而開啟多執行緒能線上程A等待時,自動切換到執行緒B,可以不浪費CPU的資源,從而能提升程式的執行 效率)。所以python的多執行緒對`IO密集型`程式碼`比較友好`。   主要要看任務的型別,我們把任務分為`I/O密集型`和`計算密集型`,而多執行緒在切換中又分為`I/O切換`和`時間切換`。如果任務屬於是I/O密集型,若不採用多執行緒,我們在進行I/O操作時,勢必要等待前面一個I/O任務完成後面的I/O任務才能進行,在這個等待的過程中,CPU處於等待狀態,這時如果採用多執行緒的話,剛好可以切換到進行另一個I/O任務。這樣就剛好可以充分利用CPU避免CPU處於閒置狀態,提高效率。但是,如果多執行緒任務都是計算型,CPU會一直在進行工作,直到一定的時間後採取多執行緒時間切換的方式進行切換執行緒,此時CPU一直處於工作狀態, 此種情況下並不能提高效能,相反在切換多執行緒任務時,可能還會`造成時間`和`資源的浪費`,導致效能下降。這就是造成上面兩種多執行緒結果不能的解釋。   **結論:**`I/O密集型`任務,建議採取多執行緒,還可以採用`多程序`+`協程`的方式(例如:爬蟲多采用多執行緒處理爬取的資料);對於`計算密集型`任務,python此時就不適用了。