Python系列:多線程(threading)的學(xué)習(xí)和使用
哈嘍大家好,我是了不起,今天來給大家介紹關(guān)于Python中的線程,threading庫。
引言
在Python中,threading庫提供了一種簡(jiǎn)單且方便的方式來實(shí)現(xiàn)多線程編程。通過使用線程,可以在程序中并行執(zhí)行多個(gè)任務(wù),提高程序的性能和響應(yīng)性。
了解線程
線程是程序執(zhí)行的最小單元,是操作系統(tǒng)能夠進(jìn)行運(yùn)算調(diào)度的基本單位。與進(jìn)程不同,線程在同一進(jìn)程下共享相同的內(nèi)存空間,因此線程之間的通信更加方便。在Python中,threading庫提供了對(duì)線程的支持。
創(chuàng)建線程
threading庫是Python中的標(biāo)準(zhǔn)庫,無需下載,我們只需在文件中導(dǎo)入threading庫就可以用了。
創(chuàng)建線程的時(shí)候主要有兩種方式,第一種是通過繼承threading.Thread類,第二種則是通過傳遞可調(diào)用對(duì)象給threading.Thread的構(gòu)造函數(shù),接下來先講解第一種方式。
1.通過繼承threading.Thread類創(chuàng)建線程
import threading
class MyThread(threading.Thread):
def __init__(self, name):
super(MyThread, self).__init__()
self.name = name
def run(self):
print(f"Thread {self.name} is running.")
# 創(chuàng)建線程的實(shí)例
thread1 = MyThread(name="Thread 1")
thread2 = MyThread(name="Thread 2")
# 啟動(dòng)線程
thread1.start()
thread2.start()
# 等待線程執(zhí)行完畢
thread1.join()
thread2.join()
print("Main thread is done.")
第一種方式是最常見的方式,創(chuàng)建線程的時(shí)候需要先創(chuàng)建一個(gè)類,然后繼承threading.Thread,然后再我們創(chuàng)建的類中自定義一個(gè)方法,這里我構(gòu)造的是run方法,在這個(gè)方法中我們可以去實(shí)現(xiàn)線程需要執(zhí)行的主要邏輯。
然后通過thread1和thread2創(chuàng)建對(duì)應(yīng)的構(gòu)造實(shí)例,使用線程中的start()方法去啟動(dòng)線程,最后在使用join()等到線程執(zhí)行完畢,這樣我們創(chuàng)建了一個(gè)基本的多線程,執(zhí)行后結(jié)果如下:
然后我們?cè)賮砹私獾诙N創(chuàng)建線程的方式。
2.通過傳遞可調(diào)用對(duì)象創(chuàng)建線程
import threading
def my_function(name):
print(f"Thread {name} is running.")
# 創(chuàng)建線程的實(shí)例,傳遞一個(gè)可調(diào)用對(duì)象和參數(shù)
thread1 = threading.Thread(target=my_function, args=("Thread 1",))
thread2 = threading.Thread(target=my_function, args=("Thread 2",))
# 啟動(dòng)線程
thread1.start()
thread2.start()
# 等待線程執(zhí)行完畢
thread1.join()
thread2.join()
print("Main thread is done.")
這種方式我們是直接通過傳遞給一個(gè)可調(diào)用對(duì)象給threading.Thread的構(gòu)造函數(shù),我們所傳遞的這個(gè)可執(zhí)行對(duì)象可以是函數(shù)、方法、或者是__call__等方法類的實(shí)例,
其中在threading.Thread實(shí)例中,通過使用target參數(shù)指定我們需要調(diào)用的對(duì)象,注意這里指定調(diào)用對(duì)象是不需要加括號(hào),直接傳需要調(diào)用的可執(zhí)行對(duì)象名就行,后面就和上面一樣,通過使用start()方法和join()方法,執(zhí)行結(jié)果也是跟第一種方式一樣。
以上兩種方式都可以創(chuàng)建線程,選擇那種一般取決于個(gè)人在項(xiàng)目中的代碼風(fēng)格和偏好,但是最終都是需要確保的是,無論使用哪種方式我們都需要保證在調(diào)用的方法中包含有線程的主要邏輯。
線程同步
Python中的線程和其他語言中的線程邏輯也是一樣,如果創(chuàng)建了多個(gè)線程,那么這幾個(gè)線程就是共享內(nèi)存,可能會(huì)導(dǎo)致數(shù)據(jù)競(jìng)爭(zhēng)和不確定的結(jié)果,所以我們需要在線程中加鎖(lock)。
1.鎖的基本用法
在python中,如果需要對(duì)線程加鎖我們就需要用到threading.lock()這個(gè)方法:
import threading
# 共享資源
counter = 0
# 創(chuàng)建鎖對(duì)象
my_lock = threading.Lock()
def increment_counter():
global counter
for _ in range(1000000):
with my_lock:
counter += 1
# 創(chuàng)建兩個(gè)線程,分別增加計(jì)數(shù)器的值
thread1 = threading.Thread(target=increment_counter)
thread2 = threading.Thread(target=increment_counter)
# 啟動(dòng)線程
thread1.start()
thread2.start()
# 等待兩個(gè)線程執(zhí)行完畢
thread1.join()
thread2.join()
print(f"Final counter value: {counter}")
在上述代碼中,我們通過創(chuàng)建了一個(gè)全局鎖對(duì)象,然后在調(diào)用的可執(zhí)行對(duì)象中,使用with語句來獲取鎖和釋放鎖,以此來確保線程共享的資源是原子的。這樣可以避免多個(gè)線程對(duì)counter的參數(shù)結(jié)果進(jìn)行數(shù)據(jù)競(jìng)爭(zhēng)。
從這個(gè)簡(jiǎn)單的代碼上我們可能看不出執(zhí)行后實(shí)際有什么不同,接下來我舉一個(gè)例子來說明沒有加鎖和加了鎖后的執(zhí)行結(jié)果。
2.不加鎖執(zhí)行
import threading
class BankAccount:
def __init__(self, balance):
self.balance = balance
def withdraw(self, amount):
current_balance = self.balance
new_balance = current_balance - amount
# 模擬取款操作的延遲
threading.Event().wait(0.1)
self.balance = new_balance
return new_balance
# 創(chuàng)建一個(gè)共享的銀行賬戶
account = BankAccount(balance=1000)
def withdraw_from_account(account, amount):
for _ in range(3):
new_balance = account.withdraw(amount)
print(f"Withdraw {amount}, New Balance: {new_balance}")
# 創(chuàng)建兩個(gè)線程進(jìn)行取款操作
thread1 = threading.Thread(target=withdraw_from_account, args=(account, 100))
thread2 = threading.Thread(target=withdraw_from_account, args=(account, 150))
# 啟動(dòng)兩個(gè)線程
thread1.start()
thread2.start()
# 等待兩個(gè)線程執(zhí)行完畢
thread1.join()
thread2.join()
print(f"Final Balance: {account.balance}")
執(zhí)行結(jié)果:
在上面這個(gè)不加鎖的實(shí)例中,我們用withdraw方法來模擬取款操作,然后通過兩個(gè)線程來對(duì)同時(shí)對(duì)賬戶進(jìn)行取款操作,但是由于這個(gè)實(shí)例中沒有加鎖,就會(huì)出現(xiàn)下面的情況:
- thread1讀取了賬戶余額(假設(shè)為1000)。
- thread2也讀取了相同的賬戶余額(仍然是1000)。
- thread1執(zhí)行取款操作,更新了賬戶余額為900。
- thread2執(zhí)行取款操作,更新了賬戶余額為850。
就這樣,本來是同一個(gè)賬戶,但是兩個(gè)線程都是各管各的,最后導(dǎo)致兩個(gè)線程都取了3次錢后,最后得出的結(jié)果是賬戶里面還剩了550元。
接下來我們?cè)倏纯醇渔i后的執(zhí)行結(jié)果:
import threading
class BankAccount:
def __init__(self, balance):
self.balance = balance
self.lock = threading.Lock()
def withdraw(self, amount):
with self.lock:
current_balance = self.balance
new_balance = current_balance - amount
# 模擬取款操作的延遲
threading.Event().wait(0.1)
self.balance = new_balance
return new_balance
# 創(chuàng)建一個(gè)共享的銀行賬戶
account = BankAccount(balance=1000)
def withdraw_from_account(account, amount):
for _ in range(3):
new_balance = account.withdraw(amount)
print(f"Withdraw {amount}, New Balance: {new_balance}")
# 創(chuàng)建兩個(gè)線程進(jìn)行取款操作
thread1 = threading.Thread(target=withdraw_from_account, args=(account, 100))
thread2 = threading.Thread(target=withdraw_from_account, args=(account, 150))
# 啟動(dòng)兩個(gè)線程
thread1.start()
thread2.start()
# 等待兩個(gè)線程執(zhí)行完畢
thread1.join()
thread2.join()
print(f"Final Balance: {account.balance}")
同樣的實(shí)例,我們通過在實(shí)例中加鎖后再去執(zhí)行,結(jié)果如下:
通過在實(shí)例中添加with self.lock后,我們保證了兩個(gè)線程訪問余額blance的原子性,不管是有多少個(gè)線程,每個(gè)線程訪問的余額始終是其他線程取錢后的最新結(jié)果,這樣就保證了代碼程序執(zhí)行后的結(jié)果是正確的。
以上是今天分享的關(guān)于Python中一些基本的線程使用,有興趣的小伙伴想要深入學(xué)習(xí)threading這個(gè)模塊的話可以在留言區(qū)打出threading,人多的話我下期就繼續(xù)更新這個(gè)模塊。