一、threading類簡介
1、threading.Thread類參數簡介
class threading.Thread(group=None, target=None, name=None, args=(), kwargs={}, *, daemon=None)
group:目前此參數為None,在實現ThreadGroup類時為將來的擴展保留。
target:target接收的是一個函數的地址,由run()方法調用執行函數中的內容。默認為無,表示未調用任何內容。
name :線程名,可自行定義。
args:target接收的是函數名,此函數的位置參數以元組的形式存放在args中,用於執行函數時調用。
kwargs :target接收的是函數名,此函數的關鍵字參數以字典的形式存放在kwargs中,用於執行函數時調用。
daemon:如果為True表示該線程為守護線程。
2、threading.Thread類的常用方法
start():開啟線程,一個Thread對象只能調用一次start()方法,如果在同一線程對象上多次調用此方法,則會引發RuntimeError。
run():執行start()方法會調用run(),該方將創建Thread對象時傳遞給target的函數名,和傳遞給args、kwargs的參數組合成一個完整的函數,並執行該函數。run()方法一般在自定義Thead類時會用到。
join(timeout=None):join會阻塞、等待線程,timeout單位為秒,因為join()總是返回none,所以在設置timeout調用join(timeout)之后,需要使用isalive()判斷線程是否執行完成,如果isalive為True表示線程在規定時間內沒有執行完,線程超時。如果join(timeout=None)則會等待線程執行完畢后才會執行join()后面的代碼,一般用於等待線程結束。
name:獲取線程名。
getName():獲取線程名。
setName(name):設置線程名。
ident:“線程標識符”,如果線程尚未啟動,則為None。如果線程啟動是一個非零整數。
is_alive():判斷線程的存活狀態,在run()方法開始之前,直到run()方法終止之后。如果線程存活返回True,否則返回False。
daemon:如果thread.daemon=True表示該線程為守護線程,必須在調用Start()之前設置此項,否則將引發RuntimeError。默認為False
isDaemon():判斷一個線程是否是守護線程。
setDaemon(daemonic):設置線程為守護線程。
二、threading.Thread的簡單使用
創建線程:
from threading import Thread def work(args,kwargs=None): print(args) print(kwargs) if __name__ == "__main__": t = Thread(target=work, args=(("我是位置參數"),), kwargs={'kwargs': '我是關鍵字參數'}, name='我是線程demo') print(t.name) # 打印線程名 t.start() # 開啟線程 print('我是主線程') # 打印內容如下 我是線程demo 我是位置參數 我是關鍵字參數 我是主線程
由上面的打印內容我們可以看出,在執行完所有線程后才執行的主線程print。如果是多進程的話會先執行主進程中的print然后才會執行子進程的print。主要是因為開啟進程相比於開啟線程更加耗費時間。
自定義線程類
from threading import Thread def work(args,kwargs=None): print(args) print(kwargs)
class MyThread(Thread): # 使用繼承Thread的方式,自定義線程類 def __init__(self,target=None, name=None, args=(), kwargs=None, *, daemon=None): # 如果要給對象封裝屬性,必須先調用父類 super().__init__() if kwargs is None: kwargs = {} self._target = target self._name = name self._args = args self._kwargs = kwargs def run(self): # 必須要有run類,因為start要調用 print(f"我重寫了Thread類的run") self._target(*self._args,**self._kwargs) if __name__ == "__main__": t = MyThread(target=work,args=(('我是位置參數'),),kwargs={'kwargs':'我是關鍵字參數'},name='我是自定義線程類') # 創建線程對象 print(t.name) # 打印線程名 t.start() # 開啟線程 print("主線程") # 打印內容如下 我是自定義線程類 我重寫了Thread類的run 主線程 我是位置參數 我是關鍵字參數
多進程和多線程的效率對比
from threading import Thread from multiprocessing import Process import time def thread_work(name): print(f"{name}") def process_work(name): print(f"{name}") if __name__ == "__main__": # 進程執行效率 pro = [] start = time.time() for i in range(3): p = Process(target=process_work,args=(("進程-"+str(i)),)) p.start() pro.append(p) for i in pro: i.join() end = time.time() print("進程運行了:%s" %(end - start)) # 線程執行效率 thread_l = [] start = time.time() for i in range(3): t = Thread(target=process_work, args=(("線程-" + str(i)),)) t.start() thread_l.append(t) for i in thread_l: i.join() end = time.time() print("進程運行了:%s" % (end - start)) # 打印內容如下 進程-0 進程-1 進程-2 進程運行了:0.18501067161560059 線程-0 線程-1 線程-2 進程運行了:0.004000186920166016
我們可以從時間上看出,線程的效率是遠遠高於進程的。
守護線程
主線程會等待所有非守護線程執行完畢后,才結束主線程。主進程是進程內的代碼結束后就結束主進程。
對比守護進程,代碼執行完畢后立即關閉守護進程,因為在主進程看來代碼執行完畢,主進程結束了,所以守護進程在代碼結束后就被結束了。
守護線程會等待主線程的結束而結束。這是因為如果主線程結束意味着程序結束,主線程會一直等着所有非守護線程結束,回收資源然后退出程序,所以當所有非守護線程結束后,守護線程結束,然后主線程回收資源,結束程序。
下面對比守護進程和守護線程的示例:
先來看守護進程:
from multiprocessing import Process def process_work(name): print(f"{name}") if __name__ == "__main__": p = Process(target=process_work,args=("守護進程")) p.daemon=True p.start() print("主進程") # 打印內容如下 主進程
只打印了主進程,也就是說守護進程還沒來得及執行程序就結束了。
再來看守護線程:
from threading import Thread def thread_work(name): print(name) if __name__ == "__main__": t = Thread(target=thread_work,args=("守護線程",)) t.daemo=True t.start() print("\n主線程") # 打印內容如下 守護線程 主線程
也許你會說是由於線程太快了,所以才執行了守護線程。下面我們在線程中阻塞一段時間,在來看看會發生什么效果。
from threading import Thread import time def thread_work(name): time.sleep(3) # 阻塞3秒 print(name) if __name__ == "__main__": t = Thread(target=thread_work,args=("守護線程",)) t.daemo=True t.start() print("\n主線程") # 打印內容如下 主線程 守護線程
守護線程還是被執行了,如果是守護進程,守護進程里的代碼是不會被執行的。
線程鎖Lock
Lock也稱線程同步鎖,互斥鎖,原子鎖,該對象只有兩個方法:
acquire(blocking=True, timeout=-1):加鎖。
參數:
blocking:當為True時表示加鎖,只允許一個線程執行被加鎖的代碼。直到遇到release()解鎖后其它線程才可以執行加鎖部分的代碼。當為False時表示不加鎖,並且不能調用release()否則會報RuntimeError。
timeout:設置加鎖時間,單位為秒。-1表示一直等待線程release()后,才允許其它線程執行加鎖的代碼。
release():釋放鎖。
未加鎖的代碼示例
from threading import Thread import time def work(): global n temp = n time.sleep(0.1) # 由於線程太快了,所以這里停頓下 n = temp -1 if __name__ == "__main__": n = 100 t_l = [] for i in range(100): t = Thread(target=work,args=()) t.start() t_l.append(t) for i in t_l: i.join() print(n) # 打印內容如下 99
在我們看來其實值應該是0的但卻是99,就因為短暫停了0.1秒導致結果發生了變化。而我們這0.1秒的停留是模擬網絡延遲或者進程調度等原因。造成了數據的結果的錯亂。這個時候我們就需要線程鎖來保證數據的安全性。
下面我們就通過給程序加鎖,來保證數據的安全性:
from threading import Thread,Lock import time def work(lock): lock.acquire() # 加鎖 global n temp = n time.sleep(0.1) # 由於線程太快了,所以這里停頓下 n = temp -1 lock.release() # 解鎖 if __name__ == "__main__": n = 100 t_l = [] lock = Lock() # 得到一把鎖對象 for i in range(100): t = Thread(target=work,args=(lock,)) t.start() t_l.append(t) for i in t_l: i.join() print(n) # 打印內容如下 0
我們會發現程序和上一個示例的運行效率上有着很大的差別。明顯加鎖后程序的運行效率降低了,我們管這種鎖叫做線程同步鎖,使原本並行的程序編程了串行所以程序的效率會慢很多,但是程序的運行結果是正確的。在程序的運行效率和數據的正確性,我們應首先確保程序的正確性然后在考慮程序的效率。
遞歸鎖RLock
死鎖與遞歸鎖:
所謂死鎖: 是指兩個或兩個以上的進程或線程在執行過程中,出現了一種互相等待的情況,它們都將無法進行下去。此時稱系統處於死鎖狀態或系統產生了死鎖,這些永遠在互相等待的進程稱為死鎖進程,如下模擬產生死鎖。
from threading import Lock,Thread def work(): # 獲取鎖 lock.acquire() print("我是工作線程 1") # 釋放鎖 lock.release() def work_2(): # 獲取鎖 lock.acquire() print("我是工作線程 2") work() # 調用工作線程1,造成死鎖 # 釋放鎖 lock.release() if __name__ == '__main__': # 生成lock實例 lock = Lock() # 開始3個線程 for i in range(3): t = Thread(target=work_2) t.start() print('我是主線程') # 打印內容如下 我是主線程 我是工作線程 2 # 此時程序處於死鎖狀態
我們可以理解為一把鎖對象只能創建一把鎖,這把鎖必須release后,才能再次使用。否則程序就會被鎖住,等待解鎖。如上面的代碼,work_2執行創建一個lock鎖,調用work又遇到一把lock鎖,而在work_2中的lock鎖沒有被解鎖,所以程序在work中等待lock解鎖,最終造成了程序出現了死鎖,下面我們使用遞歸鎖RLock來解決上面的問題。
關於RLock的用法及方法一樣,所以這里就不再重復了。
使用RLock避免死鎖:
from threading import RLock,Thread def work(): # 獲取鎖 lock.acquire() print("我是工作線程 1") # 釋放鎖 lock.release() def work_2(): # 獲取鎖 lock.acquire() print("我是工作線程 2") work() # 調用工作線程1,造成死鎖 # 釋放鎖 lock.release() if __name__ == '__main__': # 使用遞歸鎖RLock lock = RLock() # 開始3個線程 for i in range(3): t = Thread(target=work_2) t.start() print('我是主線程') # 打印內容如下 我是工作線程 2 我是工作線程 1 我是工作線程 2 我是工作線程 1 我是工作線程 2 我是主線程 我是工作線程 1
使用遞歸鎖后程序運行正常了。
條件對象Condition(lock=None)
Condition條件變量,與鎖相關聯,在實例化對象時可以給其傳入一把鎖,如果不傳,會默認創建一把遞歸鎖。目前我對它的就理解是它是一把帶通知,掛起線程功能的鎖。它可以掛起線程,然后發送通知激活線程,並且還可以加鎖,屬於一把高級鎖,下面我們來看看常用的方法。
1、Condition類的方法
class threading.Condition([lock])
acquire():加鎖、與Lock、RLock中的用法一致,這里不過多解釋。
release():解鎖、與Lock、RLock中的用法一致,這里不過多解釋。
wait(timeout=None):掛起線程,如果timeout是None則必須等到notify或notify_all后線程才會被激活,並且被激活的線程會重新獲取到一把鎖,線程被激活后從wait掛起的位置繼續向下執行。如果指定timeout超時時間,單位為秒,表示線程掛起一段時間后在繼續執行。注意:如果調用線程在調用此方法時未獲取鎖,則會引發RuntimeError。
wait_for(predicate, timeout=None):這個不知道該如何解釋。
notify(n=1):激活被掛起的線程,n表示激活n個被掛起的線程,注意:如果調用線程在調用此方法時未獲取鎖,則會引發RuntimeError。
notify_all():激活所有被掛起的線程,注意:如果調用線程在調用此方法時未獲取鎖,則會引發RuntimeError。。
2、下面是一個無聊的實例:
from threading import Condition,Thread import time def consume(): '''消費者''' global cv global num cv.acquire() while True: num -= 1 if num <= 0: print('-' * 20) cv.notify() # 激活生產者線程 cv.wait() print(f'消費數據-{num}') time.sleep(2) cv.release() def produce(): '''生產者''' global cv global num cv.acquire() while 1: num += 1 print(f'生產數據-{num}') if num >= 5: print('-' * 20) cv.notify() # 激活消費者線程 cv.wait() time.sleep(0.5) cv.release() if __name__ == '__main__': cv = Condition() # 實例化條件對象 num = 0 # 開啟線程 produce_t = Thread(target=produce, args=()) consume_t = Thread(target=consume, args=()) produce_t.start() consume_t.start()
Threading類的方法:
threading.active_count():獲取當前活動的線程對象數量。
threading.current_thread():獲取當前的線程對象。如果調用方的控制線程不是通過線程模塊創建的,則返回功能有限的虛擬線程對象。
threading.get_ident():獲取線程標識符。
threading.enumerate():這個比較厲害,可以獲取當前活動的所有線程對象的列表。該列表包括后台線程和使用current_thread()創建的虛擬線程。以列表的形式返回。
threading.main_thread():獲取主線程對象。
Threading模塊就簡單介紹到這里吧,參考文檔:https://docs.python.org/3/library/threading.html#threading.Condition.notify
下一篇:python協程簡介:https://www.cnblogs.com/caesar-id/p/10771369.html
