概述
Python3的多線程編程中常用的兩個模塊為:_thread挤茄,threading逞刷。
推薦使用threading模塊村生。畢竟_thread模塊是相當(dāng)?shù)讓拥哪K惊暴,雖然該模塊可以讓你對線程進(jìn)行細(xì)致的管理,但由于沒有提供高級函數(shù)趁桃,所以用起來比較費(fèi)勁辽话。(輪子不如threading模塊的好使)
以下內(nèi)容將基于threading模塊展開。
創(chuàng)建線程
threading模塊支持兩種創(chuàng)建線程的方式镇辉,分別是函數(shù)方式創(chuàng)建屡穗,以及使用類來包裝線程對象的方式。
""" 案例1:函數(shù)式創(chuàng)建線程 """
import threading
import time
# 線程執(zhí)行函數(shù)
def run(num):
time.sleep(1)
print(f'i am threading{num}')
if __name__ == '__main__':
# 獲取開始的時間戳
start = time.time()
# 存放線程的列表忽肛,用于阻塞線程
thread_list = list()
# 創(chuàng)建4個線程村砂,并將每個創(chuàng)建好的線程對象放到thread_list中
for i in range(1, 5):
# 創(chuàng)建線程
thread = threading.Thread(target=run, args=(i,))
# 啟動該線程
thread.start()
thread_list.append(thread)
# 只有線程全部結(jié)束,再向下執(zhí)行
for j in thread_list:
j.join()
# 結(jié)束的時間戳
end = time.time()
# 打印該程序運(yùn)行了幾秒
print(end-start)
# i am threading1
# i am threading3
# i am threading2
# i am threading4
# 1.0026652812957764
函數(shù)式創(chuàng)建線程是非常簡單的屹逛,只需要將實(shí)例化Thread對象就可以了础废。
target參數(shù)為線程的執(zhí)行函數(shù),args參數(shù)是一個元組罕模,里面可以存放該執(zhí)行函數(shù)的參數(shù)评腺,沒有可以省略。
需要注意的是淑掌,args元組參數(shù)里的逗號蒿讥,千萬不要忘。
""" 案例2:類繼承創(chuàng)建線程 """
import threading
import time
# 繼承Thread,轉(zhuǎn)變?yōu)榫€程類
class MyThread(threading.Thread):
def __init__(self, threadId):
# 必須實(shí)現(xiàn)Thread類的init方法
super().__init__()
self.threadId = threadId
# 重寫執(zhí)行函數(shù)
def run(self):
time.sleep(1)
print(f'i am thread{self.threadId}')
if __name__ == '__main__':
# 獲取開始的時間戳
start = time.time()
# 存放線程的列表芋绸,用于阻塞線程
thread_list = list()
# 創(chuàng)建4個線程媒殉,并將每個創(chuàng)建好的線程對象放到thread_list中
for i in range(1, 5):
thread = MyThread(i)
# 啟動該線程
thread.start()
thread_list.append(thread)
# 只有線程全部結(jié)束,再向下執(zhí)行
for j in thread_list:
j.join()
# 結(jié)束的時間戳
end = time.time()
# 打印該程序運(yùn)行了幾秒
print(end-start)
# i am thread1
# i am thread3
# i am thread4
# i am thread2
# 1.0024833679199219
一般來說摔敛,線程的執(zhí)行函數(shù)是沒有返回值的廷蓉。但如果需要返回值,則可以通過定義一個全局變量來獲取马昙。當(dāng)然桃犬,如果使用的是類繼承的方式創(chuàng)建,則可以直接使用類屬性來接收返回值行楞。
守護(hù)線程
守護(hù)線程作用是為其他線程提供便利服務(wù)攒暇。只要當(dāng)前主線程中尚存任何一個非守護(hù)線程沒有結(jié)束,守護(hù)線程就全部工作敢伸,只有當(dāng)最后一個非守護(hù)線程結(jié)束扯饶,守護(hù)線程隨著主線程一同結(jié)束工作恒削。
""" 案例3:守護(hù)線程 """
import threading
import time
def run():
# 無限循環(huán)打印
while True:
time.sleep(0.3)
print('i am alive!')
def fly():
# 打印3次
for i in range(3):
time.sleep(0.3)
print('you alive!')
print('you die...')
if __name__ == '__main__':
# 創(chuàng)建線程1池颈,執(zhí)行函數(shù)為run
thread1 = threading.Thread(target=run)
# 創(chuàng)建線程2,執(zhí)行函數(shù)為fly
thread2 = threading.Thread(target=fly)
# 將線程1設(shè)置為守護(hù)線程
thread1.daemon = True
# 啟動這2個線程
thread1.start()
thread2.start()
# you alive!
# i am alive!
# you alive!
# i am alive!
# i am alive!
# you alive!
# you die...
由上述案例可看出钓丰,多線程任務(wù)中躯砰,如果子線程不是守護(hù)線程,主線程運(yùn)行完畢后會進(jìn)入停止?fàn)顟B(tài)携丁,但是主線程的占用資源沒有釋放(程序不會退出)琢歇,一直到所有的子線程全部執(zhí)行完成后才會釋放資源,退出程序梦鉴。
另外李茫,需要注意,設(shè)置守護(hù)線程時一定要在start方法之前設(shè)置肥橙。
threading模塊的常用方法
關(guān)于這些方法魄宏,值得一提的是,threading模塊仍然支持 Python 2.x 的以駝峰命名實(shí)現(xiàn)的方法存筏,不過筆者還是建議使用下劃線的方法宠互。(當(dāng)然,如果是為了兼容 Python 2.x 椭坚,那就請忽略筆者的建議予跌。)
# 啟動線程
threading.start()
# 等待至該線程中止,可以達(dá)到資源獨(dú)占的作用
threading.join([time])
# 返回線程是否存活的
threading.is_alive()
# 返回當(dāng)前的Thread對象
threading.current_thread()
# 設(shè)置守護(hù)線程
threading.setDaemon(True)
# 代表線程活動的方法,可以在子類型里重載
threading.run()
# 返回一個包含正在運(yùn)行的線程的list
threading.enumerate()
# 返回正在運(yùn)行的線程數(shù)量善茎,等價于len(threading.enumerate())
threading.active_count()
# 設(shè)置線程名
threading.setName()
# 返回線程名
threading.getName()
鎖對象與共享變量
多線程同時訪問同一變量時券册,會產(chǎn)生共享變量的問題,造成變量沖突。也就是當(dāng)線程需要共享數(shù)據(jù)時烁焙,可能存在數(shù)據(jù)不同步的問題略吨。這時候可以使用threading.Lock來把線程中的變量鎖定,使用完再釋放考阱。
# 創(chuàng)建鎖對象
lock = threading.Lock()
# 獲取鎖翠忠,用于線程同步
lock.acquire()
# 釋放鎖,開啟下一個線程
lock.release()
具體使用可以參考下列案例乞榨。
""" 案例4:線程鎖 """
import threading
def func():
# 引用全局變量num
global num
# 為num循環(huán)加1
for i in range(1, 1000001):
# 為下面1行代碼上鎖
lock.acquire()
num += 1
# 解鎖上1行代碼
lock.release()
# threading.current_thread().name為打印線程名
print(threading.current_thread().name, num)
if __name__ == '__main__':
# 計數(shù)君
num = 0
# 創(chuàng)建鎖對象
lock = threading.Lock()
# 創(chuàng)建2個線程
thread1 = threading.Thread(target=func)
thread2 = threading.Thread(target=func)
# 為這兩個Thread對象(線程)起名
thread1.name = 'thread_1'
thread2.name = 'thread_2'
# 啟動這2個線程
thread1.start()
thread2.start()
# 等待線程結(jié)束
thread1.join()
thread2.join()
print('程序運(yùn)行完畢秽之!')
# thread_1 1655942
# thread_2 2000000
# 程序運(yùn)行完畢!
本文中筆者有提到吃既,可以通過使用全局變量來接收返回值考榨。案例4的代碼就相當(dāng)于是使用了num接收函數(shù)func的返回值,即num加1000000的值鹦倚。
然后河质,這個案例對線程鎖的意義在哪?實(shí)際上震叙,當(dāng)range里的數(shù)字比較小時掀鹅,對程序本身的影響并不大。然而媒楼,當(dāng)range里的數(shù)字過大時乐尊,也就是代表計算密集度非常大時,就會出現(xiàn)數(shù)據(jù)的混亂划址。
""" 去掉線程鎖時的打印結(jié)果 """
# thread_1 1101612
# thread_2 1367565
# 程序運(yùn)行完畢扔嵌!
由上述案例便可看出,線程鎖的重要性夺颤。當(dāng)然痢缎,可能有些讀者會問,為什么案例4第一條打印的是1655942而不是1000000世澜?其實(shí)呢独旷,只需要在多線程的角度思考一下就可以相通了。
最后還需要提一下宜狐,線程鎖不僅僅只能通過threading.Lock()創(chuàng)建势告,還可以使用threading.RLock()多重鎖,用法和threading.Lock()相同抚恒。因為threading.Lock()在線程中必須等待鎖釋放后(release)才能再次上鎖咱台。而threading.RLock在同一線程中可用被多次acquire。需要注意的是俭驮,在threading.RLock中acquire和release必須成對出現(xiàn)回溺。
死鎖
在線程共享多個資源的時候春贸,如果兩個線程分別占有一部分資源并且同時等待對方的資源,這種情況便會造成死鎖遗遵。
在Python中的死鎖一般都是由于threading.Lock的acquire使用不合理導(dǎo)致的萍恕。情景就類似于哲學(xué)家吃飯的問題:由于服務(wù)員的疏忽,導(dǎo)致2個哲學(xué)家手中分別拿到的是2把叉子和2把餐刀车要,其中一個哲學(xué)家說允粤,你給我叉子,我給你餐刀翼岁,另一個哲學(xué)家則說类垫,你給我餐刀,我給你叉子......
""" 案例5:死鎖 """
import threading
import time
# 哲學(xué)家a
def philosopher_a():
if lock_b.acquire():
print('我有餐刀')
print('給我叉子')
time.sleep(0.5)
if lock_a.acquire():
print('我給你餐刀')
lock_a.release()
lock_b.release()
# 哲學(xué)家b
def philosopher_b():
if lock_a.acquire():
print('我有叉子')
print('給我餐刀')
time.sleep(0.5)
if lock_b.acquire():
print('我給你叉子')
lock_b.release()
lock_a.release()
if __name__ == "__main__":
# 創(chuàng)建2個鎖對象
lock_a = threading.Lock()
lock_b = threading.Lock()
# 創(chuàng)建2個線程
thread1 = threading.Thread(target=philosopher_a)
thread2 = threading.Thread(target=philosopher_b)
# 啟動這2個線程
thread1.start()
thread2.start()
上述代碼一旦運(yùn)行琅坡,就會使程序掛死悉患,原理很簡單,就是2個鎖在互相等待對方解鎖榆俺。你不解鎖售躁,我不解鎖,那就停住咯茴晋。是不是像極了你和女朋友吵架陪捷,你不道歉,我不道歉晃跺,那就誰也不理誰咯揩局。
所以說,在程序設(shè)計中掀虎,一定要嚴(yán)格分析鎖定的數(shù)據(jù),避免死鎖的問題付枫,將死鎖扼殺在搖籃里才是最安全的烹玉。