一篇文章搞懂Python多執行緒簡單實現和GIL
今天開始打算開一個新系列,就是python的多執行緒和多程序實現,這部分可能有些新手還是比較模糊的,都知道python中的多執行緒是假的,但是又不知道怎麼回事,首先我們看一個例子來看看python多執行緒的實現。
import threading import time def say(name): print('你好%s at %s' %(name,time.ctime())) time.sleep(2) print("結束%s at %s" %(name,time.ctime())) def listen(name): print('你好%s at %s' % (name,time.ctime())) time.sleep(4) print("結束%s at %s" % (name,time.ctime())) if __name__ == '__main__': t1 = threading.Thread(target=say,args=('tony',)) #Thread是一個類,例項化產生t1物件,這裡就是建立了一個執行緒物件t1 t1.start() #執行緒執行 t2 = threading.Thread(target=listen, args=('simon',)) #這裡就是建立了一個執行緒物件t2 t2.start() print("程式結束=====================") 結果: 你好tony at Thu Apr 25 16:46:22 2019 你好simon at Thu Apr 25 16:46:22 2019 程式結束===================== 結束tony at Thu Apr 25 16:46:24 2019 結束simon at Thu Apr 25 16:46:26 2019 Process finished with exit code 0
python的多執行緒是通過threading模組的Thread類來實現的。 建立執行緒物件 t1 =
threading.Thread(target=say,args=('tony',)) #Thread是一個類,例項化產生t1物件,這裡就是建立了一個執行緒物件t1 啟動執行緒 t1.start() #執行緒執行
下面我們分析下上面程式碼的結果:
你好tony at Thu Apr 25 16:46:22 2019 --t1執行緒執行 你好simon at Thu Apr 25 16:46:22 2019 --t2執行緒執行 程式結束===================== --主執行緒執行 結束tony at Thu Apr 25 16:46:24 2019 --sleep之後,t1執行緒執行 結束simon at Thu Apr 25 16:46:26 2019 --sleep之後,t2執行緒執行 Process finished with exit code 0 --主執行緒結束
我們可以看到主執行緒的print並不是等t1,t2執行緒都執行完畢之後才打印的,這是因為主執行緒和t1,t2 執行緒是同時跑的。但是主程序要等非守護子執行緒結束之後,主執行緒才會退出。
Python學習交流群:556370268,這裡是python學習者聚集地,有大牛答疑,有資源共享!有想學習python程式設計的,或是轉行,或是大學生,還有工作中想提升自己能力的,正在學習的小夥伴歡迎加入學習。
上面其實就是python多執行緒的最簡單用法,但是可能有人會和我有一樣的需求,一般開發中,我們需要主執行緒的print列印是在最後面的,表明所有流程都結束了,也就是主執行緒結束了。這裡就引入了一個join的概念。
import threading import time def say(name): print('你好%s at %s' %(name,time.ctime())) time.sleep(2) print("結束%s at %s" %(name,time.ctime())) def listen(name): print('你好%s at %s' % (name,time.ctime())) time.sleep(4) print("結束%s at %s" % (name,time.ctime())) if __name__ == '__main__': t1 = threading.Thread(target=say,args=('tony',)) #Thread是一個類,例項化產生t1物件,這裡就是建立了一個執行緒物件t1 t1.start() #執行緒執行 t2 = threading.Thread(target=listen, args=('simon',)) #這裡就是建立了一個執行緒物件t2 t2.start() t1.join() #join等t1子執行緒結束,主執行緒列印並且結束 t2.join() #join等t2子執行緒結束,主執行緒列印並且結束 print("程式結束=====================") 結果: 你好tony at Thu Apr 25 16:57:32 2019 你好simon at Thu Apr 25 16:57:32 2019 結束tony at Thu Apr 25 16:57:34 2019 結束simon at Thu Apr 25 16:57:36 2019 程式結束=====================
上面程式碼中加入join方法後實現了,我們上面所想要的結果,主執行緒print最後執行,並且主執行緒退出,注意主執行緒執行了列印操作和主執行緒結束不是一個概念,如果子執行緒不加join,則主執行緒也會執行列印,但是主執行緒不會結束,還是需要待非守護子執行緒結束之後,主執行緒才結束。
上面的情況,主程序都需要等待非守護子執行緒結束之後,主執行緒才結束。那我們是不是注意到一點,我說的是“非守護子執行緒”,那什麼是非守護子執行緒?預設的子執行緒都是主執行緒的非守護子執行緒,但是有時候我們有需求,當主程序結束,不管子執行緒有沒有結束,子執行緒都要跟隨主執行緒一起退出,這時候我們引入一個“守護執行緒”的概念。
如果某個子執行緒設定為守護執行緒,主執行緒其實就不用管這個子執行緒了,當所有其他非守護執行緒結束,主執行緒就會退出,而守護執行緒將和主執行緒一起退出,守護主執行緒,這就是守護執行緒的意思
看看具體程式碼,我們這裡分2種情況來討論守護執行緒,加深大家的理解, 還有一點,這個方法一定要設定在start方法前面
1.設定t1執行緒為守護執行緒,看看執行結果
import threading import time def say(name): print('你好%s at %s' %(name,time.ctime())) time.sleep(2) print("結束%s at %s" %(name,time.ctime())) def listen(name): print('你好%s at %s' % (name,time.ctime())) time.sleep(4) print("結束%s at %s" % (name,time.ctime())) if __name__ == '__main__': t1 = threading.Thread(target=say,args=('tony',)) #Thread是一個類,例項化產生t1物件,這裡就是建立了一個執行緒物件t1 t1.setDaemon(True) t1.start() #執行緒執行 t2 = threading.Thread(target=listen, args=('simon',)) #這裡就是建立了一個執行緒物件t2 t2.start() print("程式結束=====================") 結果: 你好tony at Thu Apr 25 17:11:41 2019 你好simon at Thu Apr 25 17:11:41 2019 程式結束===================== 結束tony at Thu Apr 25 17:11:43 2019 結束simon at Thu Apr 25 17:11:45 2019
注意執行順序,
這裡如果設定t1為Daemon,那麼主執行緒就不管t1的執行狀態,只管等待t2結束, t2結束主執行緒就結束了
因為t2的時間4秒,t1的時間2秒,主執行緒在等待t2執行緒結束的過程中,t1執行緒自己結束了,所以結果是:
你好tony at Thu Apr 25 14:11:54 2019
你好simon at Thu Apr 25 14:11:54 2019程式結束===============
結束tony at Thu Apr 25 14:11:56 2019 (也會列印,因為主執行緒在等待t2執行緒結束的過程中, t1執行緒自己結束了)
結束simon at Thu Apr 25 14:11:58 2019
2.設定t2為守護執行緒
import threading import time def say(name): print('你好%s at %s' %(name,time.ctime())) time.sleep(2) print("結束%s at %s" %(name,time.ctime())) def listen(name): print('你好%s at %s' % (name,time.ctime())) time.sleep(4) print("結束%s at %s" % (name,time.ctime())) if __name__ == '__main__': t1 = threading.Thread(target=say,args=('tony',)) #Thread是一個類,例項化產生t1物件,這裡就是建立了一個執行緒物件t1 t1.start() #執行緒執行 t2 = threading.Thread(target=listen, args=('simon',)) #這裡就是建立了一個執行緒物件t2 t2.setDaemon(True) t2.start() print("程式結束=====================") 結果: 你好tony at Thu Apr 25 17:15:36 2019 你好simon at Thu Apr 25 17:15:36 2019 程式結束===================== 結束tony at Thu Apr 25 17:15:38 2019
注意執行順序:
這裡如果設定t2為Daemon,那麼主執行緒就不管t2的執行狀態,只管等待t1結束, t1結束主執行緒就結束了
因為t2的時間4秒,t1的時間2秒, 主執行緒在等待t1執行緒結束的過程中, t2執行緒自己結束不了,所以結果是:
你好tony at Thu Apr 25 14:14:23 2019
你好simon at Thu Apr 25 14:14:23 2019
程式結束 == == == == == == == == == == =
結束tony at Thu Apr 25 14:14:25 2019
結束simon at Thu Apr 25 14:11:58 2019 不會列印,因為主執行緒在等待t1執行緒結束的過程中, t2執行緒自己結束不了,t2的時間4秒,t1的時間2秒
不知道大家有沒有弄清楚上面python多執行緒的實現方式以及join,守護執行緒的用法。
Python學習交流群:556370268,這裡是python學習者聚集地,有大牛答疑,有資源共享!有想學習python程式設計的,或是轉行,或是大學生,還有工作中想提升自己能力的,正在學習的小夥伴歡迎加入學習。
主要方法:
join():在子執行緒完成執行之前,這個子執行緒的父執行緒將一直被阻塞。
setDaemon(True):
將執行緒宣告為守護執行緒,必須在start() 方法呼叫之前設定, 如果不設定為守護執行緒程式會被無限掛起。這個方法基本和join是相反的。
當我們在程式執行中,執行一個主執行緒,如果主執行緒又建立一個子執行緒,主執行緒和子執行緒 就分兵兩路,分別執行,那麼當主執行緒完成
想退出時,會檢驗子執行緒是否完成。如 果子執行緒未完成,則主執行緒會等待子執行緒完成後再退出。但是有時候我們需要的是 只要主執行緒
完成了,不管子執行緒是否完成,都要和主執行緒一起退出,這時就可以 用setDaemon方法啦
其他方法:
run(): 執行緒被cpu排程後自動執行執行緒物件的run方法 start():啟動執行緒活動。 isAlive(): 返回執行緒是否活動的。 getName(): 返回執行緒名。 setName(): 設定執行緒名。 threading模組提供的一些方法: threading.currentThread(): 返回當前的執行緒變數。 threading.enumerate(): 返回一個包含正在執行的執行緒的list。正在執行指執行緒啟動後、結束前,不包括啟動前和終止後的執行緒。 threading.activeCount():返回正在執行的執行緒數量,與len(threading.enumerate())有相同的結果。
上面的例子中我們注意到兩如果個任務如果順序執行要6s結束,如果是多執行緒執行4S結束,效能是有所提升的,但是我們要知道這裡的效能提升實際上是由於cpu併發實現效能提升,也就是cpu執行緒切換(多道技術)帶來的,而並不是真正的多cpu並行執行。
上面提到了並行和併發,那這兩者有什麼區別呢? 併發:是指一個系統具有處理多個任務的能力(cpu切換,多道技術) 並行:是指一個系統具有同時處理多個任務的能力(cpu同時處理多個任務) 並行是併發的一種情況,子集
那為什麼python在多執行緒中為什麼不能實現真正的並行操作呢?就是在多cpu中執行不同的執行緒(我們知道JAVA中多個執行緒可以在不同的cpu中,實現並行執行)這就要提到python中大名鼎鼎GIL,那什麼是GIL?
GIL:全域性直譯器鎖 無論你啟多少個執行緒,你有多少個cpu, Python在執行的時候只會的在同一時刻只允許一個執行緒(執行緒之間有競爭)拿到GIL在一個cpu上執行,當執行緒遇到IO等待或到達者輪詢時間的時候,cpu會做切換,把cpu的時間片讓給其他執行緒執行,cpu切換需要消耗時間和資源,所以計算密集型的功能(比如加減乘除)不適合多執行緒,因為cpu執行緒切換太多,IO密集型比較適合多執行緒。
任務: IO密集型(各個執行緒都會都各種的等待,如果有等待,執行緒切換是比較適合的),也可以採用可以用多程序+協程 計算密集型(執行緒在計算時沒有等待,這時候去切換,就是無用的切換),python不太適合開發這類功能
我們前面舉得例子裡面模擬了sleep操作,其實就是相當於遇到IO,這種場景用多執行緒是可以增加效能的,但是如果我們用多執行緒來計算資料的計算,效能反而會降低。
下面是GIL的一段原生解釋:
In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple native threads from executing Python bytecodes at once.
This lock is necessary mainly because CPython’s memory management is not thread-safe.
(However, since the GIL exists, other features have grown to depend on the guarantees that it enforces.)