線程 什麼是線程 官方定義: 線程(thread)是操作系統能夠進行運算調度的最小單位。它被包含在進程之中,是進程中的實際運作單位。一條線程指的是進程中一個單一順序的控制流,一個進程中可以併發多個線程,每條線程並行執行不同的任務。 說人話: 假如 進程 是保潔公司, 線程 就是公司的員工。當公司接到 ...
線程
什麼是線程
官方定義:
線程(thread)是操作系統能夠進行運算調度的最小單位。它被包含在進程之中,是進程中的實際運作單位。一條線程指的是進程中一個單一順序的控制流,一個進程中可以併發多個線程,每條線程並行執行不同的任務。
說人話:
假如進程是保潔公司,線程就是公司的員工。當公司接到任務時,幹活的是員工,而進程負責分配員工。可以好幾個員工擦一塊玻璃,也可以一個員工收拾一個屋子。
特點
獨立調度和分派的基本單位
一個公司里至少得有一個員工,才能幹活。公司分配各項工作給團隊,最後團隊還是會分配給個人。
輕型實體
每個線程占用的系統資源非常少。
可併發執行
多個線程可以同時工作,就像公司里的員工可以一切幹活。
共用進程資源
所有的線程共用該進程所擁有的資源。
例如: 所有線程地址空間都相同(進程的地址空間),就好比一家公司的所有員工的工作地址都為公司的所在地。
線程與進程的關係
繼續用保潔公司舉例子
- 公司本身是進程,公司里又很多員工,每個員工都是一個線程,同時公司里還有很多共用的資源,比如:掃把、墩布、毛巾、飲水機等。
- 但是與現實模型不同的是,這些人是由多個 cpu 控制的,例如 4 個 cpu 對應 40 個人,cpu 需要切換控制。
- 真正執行工作的是公司員工,也就是進程的任務靠線程執行
- 這些人可以並行工作,處理事情。也就是多線程可以同時運行。
- 當大家訪問公共資源的時候會有衝突,例如:都要出去擦玻璃,就剩一條毛巾了,這時就需要排隊。在進程中就叫做上鎖。
Python3中的多線程
全局解釋器鎖(GIL)
GIL是啥?
GIL並不是Python語言的特性,它是在現實Python解釋器時引用的一個概念。
GIL只在CPython解釋器上存在。作用是保證同一時間內只有一個線程在執行。
解決解釋器中多個線程的競爭資源問題
GIL對Python程式有啥影響?
Python中同一時刻有且只有一個線程會執行。
因此,Python中的多線程並不算是真正意義上的多線程。Python中的多個線程由於GIL鎖的存在無法利用多核CPU。
因此,Python中的多線程不適合電腦密集型的程式。
計算密集型程式
計算密集型又叫CUP密集型,這類程式絕大部分運行時間都消耗在CPU計算上,這個時候,不論你開多少線程,他用的時間都是這麼多,甚至比原來時間還長,因為GIL一個時刻只讓你執行一個線程,大部分計算密集型任務你分了很多線程但是依然會按照代碼順序線性執行。分很多線程沒有什麼改善,反而因為代碼的冗雜可能更加慢。
IO密集型程式
IO密集型顧名思義就是主要進行I/O操作,90%以上的時間都花費在網路、硬碟、輸入輸出上了,CPU執行完命令之後其實就沒事幹了,就可以釋放記憶體來執行下一條命令了,不用讓CPU在那乾等著,這樣就能大大提高程式的運行效率。
改善GIL產生的問題
- 使用更高版本的解釋器,優化對Python的解釋
- 變更解釋器,如(JPython),但可能因為相對小眾,支持的模塊相對較少,開發效率變低
- 用多進程方案替代多線程
Python3關於多線程的模塊
Python的標準庫提供了兩個模塊:_thread和threading
_thread
在Python3之前為thread,有於存在缺陷,不建議使用,在Python3中封裝成_thread
threading
thread的繼承者,絕大多數情況下使用threading就夠了。
多線程使用
直接調用
把一個函數傳入創建Thread實例,然後調用start()開始執行
import threading import time def loop(): #threading.current_thread().name獲取當前線程的名字 print(f"線程( {threading.current_thread().name} )正在執行……") num = 0 while num < 5: num += 1 print(f"線程( {threading.current_thread().name} )>>> {num}") time.sleep(1) print(f"線程( {threading.current_thread().name}) 執行結束") if __name__ == '__main__': print(f"線程( {threading.current_thread().name} )正在執行……") #創建線程實體,target導入的函數,name線程的名字,初始為Tread1,之後類推2,3,4 t = threading.Thread(target=loop,name='LoopThread') #啟動線程 t.start() #等待多線程結束 t.join() print(f"線程( {threading.current_thread().name} )執行結束")
執行結果如下:
線程( MainThread )正在執行…… 線程( LoopThread )正在執行…… 線程( LoopThread )>>> 1 線程( LoopThread )>>> 2 線程( LoopThread )>>> 3 線程( LoopThread )>>> 4 線程( LoopThread )>>> 5 線程( LoopThread) 執行結束 線程( MainThread )執行結束
繼承自threading.Thread調用
- 直接繼承Thread
- 重寫run函數,run函數代表的是真正執行的功能
- 類實例可以直接運行
import threading import time # 1. 類需要繼承自threading.Thread class MyThread(threading.Thread): def __init__(self, arg): super(MyThread, self).__init__() self.arg = arg # 2 必須重寫run函數,run函數代表的是真正執行的功能 def run(self): time.sleep(2) print(f"Run >>> {self.arg}") for i in range(1,4): t = MyThread(i) t.start() t.join() print("End")
執行結果如下:
Run >>> 1 Run >>> 2 Run >>> 3 End
守護線程
不設置守護線程
import time import threading def fun(): print("啟動Fun") time.sleep(2) print("結束Fun") print("啟動Main") t = threading.Thread(target=fun) t.start() time.sleep(1) print("結束Main")
運行結果
啟動Main 啟動Fun 結束Main 結束Fun
設置守護線程
import time import threading def fun(): print("啟動Fun") time.sleep(2) print("結束Fun") print("啟動Main") t = threading.Thread(target=fun) t.setDaemon(True) t.start() time.sleep(1) print("結束Main")
運行結果
啟動Main 啟動Fun 結束Main
如果你設置一個線程為守護線程,,就表示你認為此線程不重要,在進程退出的時候,不用等待這個線程即可退出。
thread.setDaemon(True|False)
表示此線程是否為守護線程。但此語句必須加在thread.start()
之前
常用函數
返回一個包含正在運行的線程的listthreading.enumerate()
:
返回正在運行的線程數量,效果跟 len(threading.enumerate)相同threading.activeCount()
:thr.setName()
: 給線程設置名字thr.getName()
: 得到線程的名字thr表示線程實例,如
t1.getName()
thr.getName()
獲取指定線程的名字,threading.current_thread().name
獲取當前線程的名字
共用變數
多線程同時訪問同一變數時,會產生共用變數的問題,造成變數衝突產生問題。
實例
執行多線程,對同一變數進行加減操作,代碼如下。
import threading sum = 0 loopSum = 1000000 def myAdd(): global sum, loopSum for i in range(1, loopSum): sum += 1 def myMinu(): global sum, loopSum for i in range(1, loopSum): sum -= 1 if __name__ == '__main__': print(f"Starting ....{sum}") t1 = threading.Thread(target=myAdd, args=()) t2 = threading.Thread(target=myMinu, args=()) t1.start() t2.start() t1.join() t2.join() print(f"Done .... {sum}")
兩次分別如下執行結果如下:
Starting ....0 Done .... -278763
Starting ....0 Done .... 662850
可以發現,運算結果與我們傳統認知不同。原因是Python在內部實現運算時是一個複雜的過程,同時對
sum
進行修改時產生的相互干擾,故出現未知錯誤,導致結果出錯。解決方法:上鎖
鎖(Lock):是一個標誌,表示一個線程在占用一些共用資源.
那些資源需要上鎖?
需要被共用使用的,可能產生使用衝突的
註意:
避免產生死鎖,導致程式陷入死迴圈
- 線程安全問題:
- 如果一個資源,他對於多線程來講,不用加鎖也不會引起任何問題,則稱為線程安全。
- 線程不安全變數類型: list, set, dict
- 線程安全變數類型: queue
使用案例,代碼如下:
import threading sum = 0 loopSum = 1000000 lock = threading.Lock() def myAdd(): global sum, loopSum for i in range(1, loopSum): # 上鎖,申請鎖 lock.acquire() sum += 1 # 釋放鎖 lock.release() def myMinu(): global sum, loopSum for i in range(1, loopSum): lock.acquire() sum -= 1 lock.release() if __name__ == '__main__': print(f"Starting ....{sum}") t1 = threading.Thread(target=myAdd, args=()) t2 = threading.Thread(target=myMinu, args=()) t1.start() t2.start() t1.join() t2.join() print(f"Done .... {sum}")
執行結果:
Starting ....0 Done .... 0