python線程(二)代碼部分Threading模塊


一、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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM