每一個(gè)程序的運(yùn)行都是一個(gè)進(jìn)程零如,而每個(gè)進(jìn)程至少有一個(gè)線程溜在,稱之為主線程很澄。舉個(gè)例子:食堂吃飯是一個(gè)進(jìn)程京闰,為了讓學(xué)生更快的吃上飯,我們會(huì)開多個(gè)窗口甩苛,每一個(gè)窗口就代表的是一個(gè)個(gè)的線程忙干,如果只有一個(gè)窗口,就是單線程浪藻。
我們都知道程序并不能單獨(dú)運(yùn)行捐迫,只有將程序裝載到內(nèi)存中,系統(tǒng)為它分配資源才能運(yùn)行爱葵,而這種執(zhí)行的程序就稱之為進(jìn)程施戴。程序和進(jìn)程的區(qū)別就在于:程序是指令的集合,它是進(jìn)程運(yùn)行的靜態(tài)描述文本萌丈;進(jìn)程是程序的一次執(zhí)行活動(dòng)赞哗,屬于動(dòng)態(tài)概念。在多道編程中辆雾,我們允許多個(gè)程序同時(shí)加載到內(nèi)存中肪笋,在操作系統(tǒng)的調(diào)度下,可以實(shí)現(xiàn)并發(fā)地執(zhí)行。這是這樣的設(shè)計(jì)藤乙,大大提高了CPU的利用率猜揪。進(jìn)程的出現(xiàn)讓每個(gè)用戶感覺到自己獨(dú)享CPU,因此坛梁,進(jìn)程就是為了在CPU上實(shí)現(xiàn)多道編程而提出的而姐。
進(jìn)程只能在一個(gè)時(shí)間干一件事,如果想同時(shí)干兩件事或多件事划咐,進(jìn)程就無能為力了拴念。
進(jìn)程在執(zhí)行的過程中如果阻塞,例如等待輸入褐缠,整個(gè)進(jìn)程就會(huì)掛起政鼠,即使進(jìn)程中有些工作不依賴于輸入的數(shù)據(jù),也將無法執(zhí)行队魏。
地址空間和其它資源(如打開文件):進(jìn)程間相互獨(dú)立公般,同一進(jìn)程的各線程間共享。某進(jìn)程內(nèi)的線程在其它進(jìn)程不可見器躏。
通信:進(jìn)程間通信IPC俐载,線程間可以直接讀寫進(jìn)程數(shù)據(jù)段(如全局變量)來進(jìn)行通信——需要進(jìn)程同步和互斥手段的輔助,以保證數(shù)據(jù)的一致性登失。
調(diào)度和切換:線程上下文切換比進(jìn)程上下文切換要快得多遏佣。
在多線程操作系統(tǒng)中,進(jìn)程不是一個(gè)可執(zhí)行的實(shí)體揽浙。
threading.Thread()類
參數(shù)名 | 含義 |
---|---|
target | 線程調(diào)用的對象状婶,就是目標(biāo)函數(shù) |
name | 為線程起個(gè)名字 |
args | 為目標(biāo)函數(shù)傳遞實(shí)參,元祖 |
kwargs | 為目標(biāo)函數(shù)關(guān)鍵字傳參馅巷,字典 |
線程的啟動(dòng)
# 線程啟動(dòng)
import threading
# 最簡單的線程程序
def worker():
print('I am working')
print('Fineshed')
t = threading.Thread(target=worker, name='worker') # 線程對象
t.start() # 啟動(dòng)
通過threading.Thread創(chuàng)建一個(gè)線程對象膛虫,target是目標(biāo)函數(shù),name可以指定名稱钓猬。
但是線程沒有啟動(dòng)稍刀,需要調(diào)用start方法。
線程之所以執(zhí)行函數(shù)敞曹,是因?yàn)榫€程中就是執(zhí)行代碼的账月,而最簡單的的封裝就是函數(shù),所以還是函數(shù)調(diào)用澳迫。
函數(shù)執(zhí)行完局齿,線程也就退出了。
線程的退出
Python沒有提供線程退出的方法橄登,線程在下面情況時(shí)退出:
1抓歼、線程函數(shù)內(nèi)語句執(zhí)行完畢
2讥此、線程函數(shù)中拋出未處理的異常
# 線程的退出
import threading
import time
def worker():
count = 0
while True:
if count > 5:
break
time.sleep(2)
print("I'm working")
count += 1
t = threading.Thread(target=worker, name='worker') # 線程對象
t.start() # 啟動(dòng)
print('==End==')
# 輸出結(jié)果
==End==
I'm working
I'm working
I'm working
I'm working
I'm working
I'm working
python的線程沒有優(yōu)先級、沒有線程組的概念谣妻,也不能被銷毀萄喳、停止、掛起拌禾,那也就沒有恢復(fù)取胎、中斷了展哭。
線程的傳參
# 線程的傳參
import threading
import time
def add(x,y):
print("{} + {} = {}".format(x, y, x+y, threading.current_thread().ident))
thread1 = threading.Thread(target=add, name='add', args=(4,5)) # 線程對象
thread1.start() # 啟動(dòng)線程
time.sleep(2)
thread2 = threading.Thread(target=add, name='add', args=(5,), kwargs={'y':4}) # 線程對象
thread2.start() # 啟動(dòng)線程
time.sleep(2)
thread3 = threading.Thread(target=add, name='add', kwargs={'x':4, 'y':5}) #線程對象
thread3.start()
線程傳參和函數(shù)傳參沒什么區(qū)別湃窍,本質(zhì)上就是函數(shù)傳參。
threading的屬性和方法
名稱 | 含義 |
---|---|
name | 只是一個(gè)名字匪傍,一個(gè)標(biāo)識您市,名稱可以重名。getName(), setName()獲取役衡、設(shè)置這個(gè)名詞 |
ident | 線程ID茵休,它是非0整數(shù)。線程啟動(dòng)后才會(huì)有ID手蝎,否則為None榕莺。線程退出,此ID依舊可以訪問棵介。此ID可以重復(fù)使用 |
is_alive() | 返回線程是否活著 |
注意:線程的name钉鸯,這是一個(gè)名稱,可以重復(fù)邮辽;ID必須唯一唠雕,但可以在線程退出后再利用。
import threading
import time
def showthreadinfo():
print("currentthread = {}".format(threading.current_thread()))
print("main thread = {}".format(threading.main_thread()))
print("active count = {}".format(threading.active_count()))
def worker():
count = 0
showthreadinfo()
while True:
if (count > 5):
break
time.sleep(1)
count += 1
print("I'm working")
t = threading.Thread(target=worker, name='worker') # 線程對象
showthreadinfo()
t.start() # 啟動(dòng)
print('==End==')
# 輸出:
currentthread = <_MainThread(MainThread, started 4320764736)>
main thread = <_MainThread(MainThread, started 4320764736)>
active count = 1
currentthread = <Thread(worker, started 123145549832192)>
==End==
main thread = <_MainThread(MainThread, stopped 4320764736)>
active count = 2
I'm working
I'm working
I'm working
I'm working
I'm working
I'm working
名稱 | 含義 |
---|---|
start() | 啟動(dòng)線程吨述。每一個(gè)線程必須且只能執(zhí)行該方法一次 |
run() | 運(yùn)行線程函數(shù) |
start() 方法
import time
def worker():
count = 0
while True:
if (count > 5):
break
time.sleep(1)
count += 1
print("worker running")
class MyThread(threading.Thread):
def start(self):
print('start~~~~~~~~~~~')
super().start() # 調(diào)父類(就是Thread類)的start()
def run(self):
print('run~~~~~~~~~~~~~')
super().run() # 調(diào)父類的run方法
t = MyThread(name='worker', target=worker)
t.start()
#t.run()
# t.start()運(yùn)行結(jié)果
start~~~~~~~~~~~
run~~~~~~~~~~~~~
worker running
worker running
worker running
worker running
worker running
worker running
# t.run()運(yùn)行結(jié)果
run~~~~~~~~~~~~~
worker running
worker running
worker running
worker running
worker running
worker running
start()方法會(huì)調(diào)用run()方法岩睁,而run()方法可以運(yùn)行函數(shù)
使用start方法啟動(dòng)線程,是啟動(dòng)了一個(gè)新的線程揣云。但是使用run方法并沒有啟動(dòng)新的線程捕儒,就是在主線程中調(diào)用了一個(gè)普通的函數(shù)而已。
因此邓夕,啟動(dòng)線程請使用start方法刘莹,才能啟動(dòng)多個(gè)線程。
多線程
一個(gè)進(jìn)程中有多個(gè)線程翎迁,實(shí)現(xiàn)一種并發(fā)
import threading
import time
def worker():
count = 0
while True:
if (count > 3):
break
time.sleep(1)
count += 1
print("worker running")
print(threading.current_thread().name, threading.current_thread().ident)
class MyThread(threading.Thread):
def start(self):
print('start~~~~~~~~~~~')
super().start() # 調(diào)父類(就是Thread類)的start()
def run(self):
print('run~~~~~~~~~~~~~')
super().run() # 調(diào)父類的run方法
t1 = MyThread(name='worker1', target=worker)
t2 = MyThread(name='worker2', target=worker)
t1.start()
t2.start()
# 運(yùn)行結(jié)果
start~~~~~~~~~~~
run~~~~~~~~~~~~~
start~~~~~~~~~~~
run~~~~~~~~~~~~~
worker running
worker1 123145457434624
worker running
worker2 123145462689792
worker running
worker running
worker1 123145457434624
worker2 123145462689792
worker running
worker running
worker2 123145462689792
worker1 123145457434624
worker running
worker running
worker1 123145457434624
worker2 123145462689792
線程安全
關(guān)于線程安全栋猖,有一個(gè)經(jīng)典的“銀行取錢”問題。從銀行取錢的基本流程基本上可以分為如下幾個(gè)步驟:
用戶輸入賬戶汪榔、密碼蒲拉,系統(tǒng)判斷用戶的賬戶肃拜、密碼是否匹配。
用戶輸入取款金額雌团。
系統(tǒng)判斷賬戶余額是否大于取款金額燃领。
如果余額大于取款金額,則取款成功锦援;如果余額小于取款金額猛蔽,則取款失敗。
乍一看上去灵寺,這確實(shí)就是日常生活中的取款流程曼库,這個(gè)流程沒有任何問題。但一旦將這個(gè)流程放在多線程并發(fā)的場景下略板,就有可能出現(xiàn)問題毁枯。注意,此處說的是有可能叮称,并不是說一定种玛。也許你的程序運(yùn)行了一百萬次都沒有出現(xiàn)問題,但沒有出現(xiàn)問題并不等于沒有問題瓤檐!
按照上面的流程編寫取款程序赂韵,井使用兩個(gè)線程來模擬模擬兩個(gè)人使用同一個(gè)賬戶井發(fā)取錢操作。此處忽略檢查賬戶和密碼的操作挠蛉,僅僅模擬后面三步操作祭示。下面先定義一個(gè)賬戶類,該賬戶類封裝了賬戶編號和余額兩個(gè)成員變量碌秸。
import threading
import time
class Account:
# 定義構(gòu)造器
def __init__(self, account_no, balance):
# 封裝賬戶編號绍移、賬戶余額的兩個(gè)成員變量
self.account_no = account_no
self.balance = balance
# 定義一個(gè)函數(shù)來模擬取錢操作
def draw(account, draw_amount):
# 賬戶余額大于取錢數(shù)目
if account.balance >= draw_amount:
# 吐出鈔票
print(threading.current_thread().name\
+ "取錢成功!吐出鈔票:" + str(draw_amount))
# time.sleep(0.001)
# 修改余額
account.balance -= draw_amount
print("\t余額為: " + str(account.balance))
else:
print(threading.current_thread().name\
+ "取錢失敿サ纭蹂窖!余額不足!")
# 創(chuàng)建一個(gè)賬戶
acct =Account("1234567" , 1000)
# 模擬兩個(gè)線程對同一個(gè)賬戶取錢
threading.Thread(name='甲', target=draw , args=(acct , 800)).start()
threading.Thread(name='乙', target=draw , args=(acct , 800)).start()
先不要管程序中那行被注釋掉的代碼恩敌,上面程序是一個(gè)非常簡單的取錢邏輯瞬测,這個(gè)取錢邏輯與實(shí)際的取錢操作也很相似。
多次運(yùn)行上面程序纠炮,很有可能都會(huì)看到如下所示的錯(cuò)誤結(jié)果月趟。
>> 甲取錢成功!吐出鈔票:800
乙方取錢成功恢口!吐出鈔票:800
余額為200
余額為-200
運(yùn)行結(jié)果并不是銀行所期望的結(jié)果(不過有可能看到正確的運(yùn)行結(jié)果)孝宗,這正是多線程編程突然出現(xiàn)的“偶然” 錯(cuò)誤因?yàn)榫€程調(diào)度的不確定性。
假設(shè)系統(tǒng)線程調(diào)度器在注釋代碼處暫停耕肩,讓另一個(gè)線程執(zhí)行(為了強(qiáng)制暫停因妇,只要取消程序中注釋代碼前的注釋即可)问潭。取消注釋后,再次運(yùn)行程序婚被,將總可以看到如圖 1 所示的錯(cuò)誤結(jié)果狡忙。
問題出現(xiàn)了,賬戶余額只有 1000 元時(shí)取出了 1600 元址芯,而且賬戶余額出現(xiàn)了負(fù)值灾茁,遠(yuǎn)不是銀行所期望的結(jié)果。雖然上面程序是人為地使用 time.sleep(0.001) 來強(qiáng)制線程調(diào)度切換谷炸,但這種切換也是完全可能發(fā)生的(100000 次操作只要有 1 次出現(xiàn)了錯(cuò)誤北专,那就是由編程錯(cuò)誤引起的)。
同步鎖(Lock)
之所以出現(xiàn)錯(cuò)誤結(jié)果淑廊,是因?yàn)?run() 方法的方法體不具有線程安全性逗余,程序中有兩個(gè)并發(fā)線程在修改 Account 對象特咆,而且系統(tǒng)恰好在注釋代碼處執(zhí)行線程切換季惩,切換到另一個(gè)修改 Account 對象的線程,所以就出現(xiàn)了問題腻格。
為了解決這個(gè)問題画拾,Python 的 threading 模塊引入了鎖(Lock)。threading 模塊提供了 Lock 和 RLock 兩個(gè)類菜职,它們都提供了如下兩個(gè)方法來加鎖和釋放鎖:
- acquire(blocking=True, timeout=-1):請求對 Lock 或 RLock 加鎖青抛,其中 timeout 參數(shù)指定加鎖多少秒。
- release():釋放鎖酬核。
Lock 和 RLock 的區(qū)別如下:
- threading.Lock:它是一個(gè)基本的鎖對象蜜另,每次只能鎖定一次,其余的鎖請求嫡意,需等待鎖釋放后才能獲取举瑰。
- threading.RLock:它代表可重入鎖(Reentrant Lock)。對于可重入鎖蔬螟,在同一個(gè)線程中可以對它進(jìn)行多次鎖定此迅,也可以多次釋放。如果使用 RLock旧巾,那么 acquire() 和 release() 方法必須成對出現(xiàn)耸序。如果調(diào)用了 n 次 acquire() 加鎖,則必須調(diào)用 n 次 release() 才能釋放鎖鲁猩。
由此可見坎怪,RLock 鎖具有可重入性。也就是說廓握,同一個(gè)線程可以對已被加鎖的 RLock 鎖再次加鎖搅窿,RLock 對象會(huì)維持一個(gè)計(jì)數(shù)器來追蹤 acquire() 方法的嵌套調(diào)用炸客,線程在每次調(diào)用 acquire() 加鎖后,都必須顯式調(diào)用 release() 方法來釋放鎖戈钢。所以痹仙,一段被鎖保護(hù)的方法可以調(diào)用另一個(gè)被相同鎖保護(hù)的方法。
Lock 是控制多個(gè)線程對共享資源進(jìn)行訪問的工具殉了。通常开仰,鎖提供了對共享資源的獨(dú)占訪問,每次只能有一個(gè)線程對 Lock 對象加鎖薪铜,線程在開始訪問共享資源之前應(yīng)先請求獲得 Lock 對象众弓。當(dāng)對共享資源訪問完成后,程序釋放對 Lock 對象的鎖定隔箍。
在實(shí)現(xiàn)線程安全的控制中谓娃,比較常用的是 RLock。通常使用 RLock 的代碼格式如下:
class X:
#定義需要保證線程安全的方法
def m () :
#加鎖
self.lock.acquire()
try :
#需要保證線程安全的代碼
#...方法體
#使用finally 塊來保證釋放鎖
finally :
#修改完成蜒滩,釋放鎖
self.lock.release()
使用 RLock 對象來控制線程安全滨达,當(dāng)加鎖和釋放鎖出現(xiàn)在不同的作用范圍內(nèi)時(shí),通常建議使用 finally 塊來確保在必要時(shí)釋放鎖俯艰。
通過使用 Lock 對象可以非常方便地實(shí)現(xiàn)線程安全的類捡遍,線程安全的類具有如下特征:
該類的對象可以被多個(gè)線程安全地訪問。
每個(gè)線程在調(diào)用該對象的任意方法之后竹握,都將得到正確的結(jié)果画株。
每個(gè)線程在調(diào)用該對象的任意方法之后,該對象都依然保持合理的狀態(tài)啦辐。
總的來說谓传,不可變類總是線程安全的,因?yàn)樗膶ο鬆顟B(tài)不可改變芹关;但可變對象需要額外的方法來保證其線程安全续挟。例如,上面的 Account 就是一個(gè)可變類充边,它的 self.account_no和self._balance(為了更好地封裝庸推,將 balance 改名為 _balance)兩個(gè)成員變量都可以被改變,當(dāng)兩個(gè)錢程同時(shí)修改 Account 對象的 self._balance 成員變量的值時(shí)浇冰,程序就出現(xiàn)了異常贬媒。下面將 Account 類對 self.balance 的訪問設(shè)置成線程安全的,那么只需對修改 self.balance 的方法增加線程安全的控制即可肘习。
將 Account 類改為如下形式际乘,它就是線程安全的:
import threading
import time
class Account:
# 定義構(gòu)造器
def __init__(self, account_no, balance):
# 封裝賬戶編號、賬戶余額的兩個(gè)成員變量
self.account_no = account_no
self._balance = balance
self.lock = threading.RLock()
# 因?yàn)橘~戶余額不允許隨便修改漂佩,所以只為self._balance提供getter方法
def getBalance(self):
return self._balance
# 提供一個(gè)線程安全的draw()方法來完成取錢操作
def draw(self, draw_amount):
# 加鎖
self.lock.acquire()
try:
# 賬戶余額大于取錢數(shù)目
if self._balance >= draw_amount:
# 吐出鈔票
print(threading.current_thread().name\
+ "取錢成功!吐出鈔票:" + str(draw_amount))
time.sleep(0.001)
# 修改余額
self._balance -= draw_amount
print("\t余額為: " + str(self._balance))
else:
print(threading.current_thread().name\
+ "取錢失敗调衰!余額不足!")
finally:
# 修改完成征堪,釋放鎖
self.lock.release()
上面程序中的定義了一個(gè) RLock 對象。在程序中實(shí)現(xiàn) draw() 方法時(shí)关拒,進(jìn)入該方法開始執(zhí)行后立即請求對 RLock 對象加鎖佃蚜,當(dāng)執(zhí)行完 draw() 方法的取錢邏輯之后,程序使用 finally 塊來確保釋放鎖着绊。
程序中 RLock 對象作為同步鎖谐算,線程每次開始執(zhí)行 draw() 方法修改 self.balance 時(shí),都必須先對 RLock 對象加鎖归露。當(dāng)該線程完成對 self._balance 的修改洲脂,將要退出 draw() 方法時(shí),則釋放對 RLock 對象的鎖定剧包。這樣的做法完全符合“加鎖→修改→釋放鎖”的安全訪問邏輯恐锦。
當(dāng)一個(gè)線程在 draw() 方法中對 RLock 對象加鎖之后,其他線程由于無法獲取對 RLock 對象的鎖定玄捕,因此它們同時(shí)執(zhí)行 draw() 方法對 self._balance 進(jìn)行修改踩蔚。這意味著,并發(fā)線程在任意時(shí)刻只有一個(gè)線程可以進(jìn)入修改共享資源的代碼區(qū)(也被稱為臨界區(qū))枚粘,所以在同一時(shí)刻最多只有一個(gè)線程處于臨界區(qū)內(nèi),從而保證了線程安全飘蚯。
為了保證 Lock 對象能真正“鎖定”它所管理的 Account 對象馍迄,程序會(huì)被編寫成每個(gè) Account 對象有一個(gè)對應(yīng)的 Lock(就像一個(gè)房間有一個(gè)鎖一樣)。
上面的 Account 類增加了一個(gè)代表取錢的 draw() 方法局骤,并使用 Lock 對象保證該 draw() 方法的線程安全攀圈,而且取消了 setBalance() 方法(避免程序直接修改 self._balance 成員變量),因此線程執(zhí)行體只需調(diào)用 Account 對象的 draw() 方法即可執(zhí)行取錢操作峦甩。
下面程序創(chuàng)建并啟動(dòng)了兩個(gè)取錢線程:
import threading
import Account
# 定義一個(gè)函數(shù)來模擬取錢操作
def draw(account, draw_amount):
# 直接調(diào)用account對象的draw()方法來執(zhí)行取錢操作
account.draw(draw_amount)
# 創(chuàng)建一個(gè)賬戶
acct = Account.Account("1234567" , 1000)
# 模擬兩個(gè)線程對同一個(gè)賬戶取錢
threading.Thread(name='甲', target=draw , args=(acct , 800)).start()
threading.Thread(name='乙', target=draw , args=(acct , 800)).start()
上面程序中代表線程執(zhí)行體的 draw() 函數(shù)無須自己實(shí)現(xiàn)取錢操作赘来,而是直接調(diào)用 account 的 draw() 方法來執(zhí)行取錢操作。由于 draw() 方法己經(jīng)使用 RLock 對象實(shí)現(xiàn)了線程安全凯傲,因此上面程序就不會(huì)導(dǎo)致線程安全問題犬辰。
多次重復(fù)運(yùn)行上面程序,總可以看到如下運(yùn)行結(jié)果冰单。
甲取錢成功幌缝!吐出鈔票:800
余額為: 200
乙取錢失敗诫欠!余額不足涵卵!
可變類的線程安全是以降低程序的運(yùn)行效率作為代價(jià)的浴栽,為了減少線程安全所帶來的負(fù)面影響,程序可以采用如下策略:
不要對線程安全類的所有方法都進(jìn)行同步轿偎,只對那些會(huì)改變競爭資源(競爭資源也就是共享資源)的方法進(jìn)行同步典鸡。例如,上面 Account 類中的 account_no 實(shí)例變量就無須同步坏晦,所以程序只對 draw() 方法進(jìn)行了同步控制椿每。
如果可變類有兩種運(yùn)行環(huán)境,單線程環(huán)境和多線程環(huán)境英遭,則應(yīng)該為該可變類提供兩種版本间护,即線程不安全版本和線程安全版本。在單線程環(huán)境中使用錢程不安全版本以保證性能挖诸,在多線程環(huán)境中使用線程安全版本汁尺。
daemon線程和non-daemon線程
進(jìn)程靠線程執(zhí)行代碼,至少有一個(gè)主線程多律,其他線程是工作線程痴突。主線程是第一個(gè)啟動(dòng)的線程。
父線程:如果線程A中啟動(dòng)了一個(gè)線程B狼荞,A就是B的父線程 子線程:B就是A的子線程
Python中辽装,構(gòu)造線程的時(shí)候,可以設(shè)置daemon屬性相味,這個(gè)屬性必須在start方法前設(shè)置好
源碼:
# 在Thread的__init__方法中
if daemon is not None:
self._daemonic = daemon # 用戶設(shè)定bool值
else:
self._daemonic = current_thread().daemon
self._ident = None
線程daemon屬性拾积,如果設(shè)定就是用戶的設(shè)置,否則就取當(dāng)前線程的daemon值丰涉。
主線程是non-daemon線程拓巧,即daemon = False
import time
import threading
def foo():
time.sleep(2)
for i in range(5):
print(i)
# 主線程是non-daemon線程
t = threading.Thread(target=foo, daemon=False)
t.start()
print('Main Thread Exiting')
# 輸出結(jié)果
Main Thread Exiting
0
1
2
3
4
發(fā)現(xiàn)線程t依然執(zhí)行,主線程已經(jīng)執(zhí)行完一死,但是一直等著線程t肛度。
修改為t = threading.Thread(target=foo, daemon=True)后
# 輸出結(jié)果
Main Thread Exiting
名稱 | 含義 |
---|---|
daemon屬性 | 表示線程是否是daemon線程,這個(gè)值必須在start()之前設(shè)置投慈,否則引發(fā)RuntimeError異常 |
isDaemon() | 是否是daemon線程 |
setDaemon | 設(shè)置為daemon線程承耿,必須在start方法之前設(shè)置 |
會(huì)發(fā)現(xiàn)程序立即結(jié)束了,根本沒有等線程t伪煤。
名稱 | 含義 |
---|---|
daemon屬性 | 表示線程是否是daemon線程加袋,這個(gè)值必須在start()之前設(shè)置,否則引發(fā)RuntimeError異常 |
isDaemon() | 是否是daemon線程 |
setDaemon | 設(shè)置為daemon線程带族,必須在start方法之前設(shè)置 |
總結(jié):
- 線程具有一個(gè)daemon屬性锁荔,可以顯示設(shè)置為True或False,也可以不設(shè)置,則取默認(rèn)值None阳堕。如果不設(shè)置daemon跋理,就取當(dāng)前線程的daemon來設(shè)置它。
- 主線程是non-daemon線程恬总,即daemon=False拭卿。從主線程創(chuàng)建的所有線程的不設(shè)置daemon屬性,則默認(rèn)都是daemon=False贱纠,也就是non-daemon線程。
- Python程序在沒有活著的non-daemon線程運(yùn)行時(shí)退出,也就是剩下的只能是daemon線程,主線程才能退出,否則主線程就只能等待。
思考下面程序的輸出:
import time
import threading
def bar():
time.sleep(5)
print('bar')
def foo():
for i in range(10):
print(i)
t = threading.Thread(target=bar, daemon=False)
t.start()
# 主線程是non-daemon線程
t = threading.Thread(target=foo, daemon=True)
t.start()
print('Main Thread Exiting')
# 輸出結(jié)果
0
Main Thread Exiting
可以看到炮车,并不會(huì)輸出‘bar’這個(gè)字符串扛或,進(jìn)行修改:
time.sleep(2) # 在原先print上面加上這一句
print('Main Thread Exiting')
# 輸出結(jié)果
0
1
2
3
4
5
6
7
8
9
Main Thread Exiting
bar
可以看到‘bar’字符串打印出來了
再看一個(gè)例子,看看主線程什么時(shí)候結(jié)束daemon線程
# 看看主線程何時(shí)結(jié)束daemon線程
import time
import threading
def foo(n):
for i in range(n):
print(i)
time.sleep(1)
t1 = threading.Thread(target=foo, args=(5,), daemon=True)
t1.start()
t2 = threading.Thread(target=foo, args=(10,), daemon=False)
t2.start()
time.sleep(2)
print('Main Thread Exiting')
# 輸出結(jié)果
0
0
1
1
Main Thread Exiting
2
2
3
3
4
4
5
6
7
8
9
調(diào)換10和5看看效果
import time
import threading
def foo(n):
for i in range(n):
print(i)
time.sleep(1)
t1 = threading.Thread(target=foo, args=(10,), daemon=True) # 調(diào)換10和20看看效果
t1.start()
t2 = threading.Thread(target=foo, args=(5,), daemon=False)
t2.start()
time.sleep(2)
print('Main Thread Exiting')
# 輸出結(jié)果
0
0
1
1
Main Thread Exiting
2
2
3
3
4
4
5
上例說明柳爽,如果有non-daemon線程的時(shí)候娩脾,主線程退出時(shí)闹瞧,也不會(huì)殺掉所有的daemon線程洽腺,直到所有non-daemon線程全部結(jié)束藕坯,如果還有daemon線程辐马,主線程需要退出,會(huì)結(jié)束所有daemon線程、退出。
join方法
mport time
import threading
def foo(n):
for i in range(n):
print(i)
time.sleep(1)
t1 = threading.Thread(target=foo, args=(10,), daemon=True)
t1.start()
t1.join()
print("Main Thread Exiting")
# 輸出結(jié)果
0
1
2
3
4
5
6
7
8
9
Main Thread Exiting
然后取消join方法看一下結(jié)果:
# 輸出結(jié)果
0
Main Thread Exiting
使用了join方法后揍障,daemon線程執(zhí)行完了目养,主線程才退出來癌蚁。
join(timeout=None)是線程的標(biāo)準(zhǔn)方法之一。一個(gè)線程中調(diào)用另一個(gè)線程的join方法缕减,調(diào)用者將被阻塞皱卓,直到被調(diào)用線程終止总放。一個(gè)線程可以被join多次甥啄。
timeout參數(shù)指定調(diào)用者多久存炮,沒有設(shè)置超時(shí),就一直等到被調(diào)用線程結(jié)束。調(diào)用誰的join方法穆桂,就是join誰宫盔,就要等誰。
daemon線程應(yīng)用場景
daemon thread的作用是:當(dāng)你把一個(gè)線程設(shè)置為daemon享完,它會(huì)隨主線程的退出而退出灼芭。
主要應(yīng)用場景有:
- 后臺(tái)任務(wù)。如:發(fā)送心跳包般又、監(jiān)控彼绷,這種場景最多
- 主線程工作才有用的線程。如主線程中維護(hù)這公共的資源茴迁,主線程已經(jīng)清理了寄悯,準(zhǔn)備退出,而工作線程使用這些資源工作也沒有意義了堕义,一起退出最合適猜旬。
- 隨時(shí)可以被終止的線程。如果主線程退出倦卖,想所有其他工作線程一起退出洒擦,就使用daemon=True來創(chuàng)建工作線程。
比如:開啟一個(gè)線程定時(shí)判斷WEB服務(wù)是否正常工作怕膛,主線程退出熟嫩,工作線程也應(yīng)該隨著主線程退出一起退出。這種daemon線程一旦創(chuàng)建嘉竟,就可以忘記它了邦危,只看關(guān)系主線程什么時(shí)候退出就行了。簡言之舍扰,daemon線程倦蚪,簡化了程序員手動(dòng)關(guān)閉線程的工作。 - 如果在non-daemon線程A中边苹,對另一個(gè)daemon線程B使用了join方法陵且,這個(gè)線程B設(shè)置成daemon就沒有什么意義了,因?yàn)閚on-daemon線程A總是要等待B个束。
- 如果在一個(gè)daemon線程C中慕购,對另一個(gè)daemon線程D使用了join方法,只能說明C要等待D茬底,主線程退出沪悲,C和D不管是否結(jié)束,也不管他們誰等誰阱表,都要被殺掉
import time
import threading
def bar():
while True:
time.sleep(1)
print('bar')
def foo():
print("t1's daemon = {}".format(threading.current_thread().isDaemon()))
t2 = threading.Thread(target=bar)
t2.start()
print("t2's daemon = {}".format(t2.isDaemon()))
t1 = threading.Thread(target=foo, daemon=True)
t1.start()
time.sleep(3)
print("main thread exiting")
# 輸出結(jié)果:
t1's daemon = True
t2's daemon = True
bar
bar
bar
main thread exiting
上例殿如,只要主線程退出贡珊,2個(gè)工作線程都結(jié)束,可以使用join涉馁,讓線程結(jié)束不了门岔。
threading.local類
import threading
import time
def worker(): # 局部變量實(shí)現(xiàn)
x = 0
for i in range(100):
time.sleep(0.001)
x += 1
print(threading.current_thread(), x)
for i in range(10):
threading.Thread(target=worker).start()
# 輸出結(jié)果
<Thread(Thread-1, started 7128)> 100
<Thread(Thread-2, started 12008)> 100
<Thread(Thread-3, started 11536)> 100
<Thread(Thread-4, started 9792)> 100
<Thread(Thread-5, started 11608)> 100
<Thread(Thread-7, started 2340)> 100
<Thread(Thread-6, started 12028)> 100
<Thread(Thread-8, started 12204)> 100
<Thread(Thread-9, started 11948)> 100
<Thread(Thread-10, started 11816)> 100
上例使用多線程,每個(gè)線程完成不同的計(jì)算任務(wù)烤送。x是局部變量寒随。
能否改造成使用全局變量完成?
import threading
import time
class A:
def __init__(self):
self.x = 0
global_data = A() # 全局變量
def worker():
global_data.x = 0
for i in range(100):
time.sleep(0.001)
global_data.x += 1
print(threading.current_thread(), global_data.x)
for i in range(10):
threading.Thread(target=worker).start()
# 輸出結(jié)果
<Thread(Thread-2, started 11012)> 984
<Thread(Thread-4, started 11512)> 985
<Thread(Thread-3, started 9020)> 987
<Thread(Thread-5, started 9436)> 988
<Thread(Thread-1, started 11660)> 989
<Thread(Thread-7, started 8404)> 990
<Thread(Thread-8, started 7400)> 991
<Thread(Thread-6, started 8236)> 993
<Thread(Thread-10, started 4476)> 994
<Thread(Thread-9, started 11760)> 995
上例雖然使用了全局對象帮坚,但是線程之間互相干擾妻往,導(dǎo)致了錯(cuò)誤的結(jié)果。
能不能使用全局對象叶沛,還能保持每個(gè)線程使用不同的數(shù)據(jù)呢蒲讯?Python提供了threading.local類,將這個(gè)實(shí)例得到一個(gè)全局對象灰署,但是不同的線程使用這個(gè)對象存儲(chǔ)的數(shù)據(jù)其他線程看不見判帮。
import threading
import time
# 全局變量
global_data = threading.local()
def worker():
global_data.x = 0
for i in range(100):
time.sleep(0.001)
global_data.x += 1
print(threading.current_thread(), global_data.x)
for i in range(10):
threading.Thread(target=worker).start()
# 打印結(jié)果:
<Thread(Thread-3, started 5220)> 100
<Thread(Thread-7, started 4016)> 100
<Thread(Thread-4, started 11504)> 100
<Thread(Thread-2, started 10376)> 100
<Thread(Thread-1, started 11556)> 100
<Thread(Thread-8, started 8908)> 100
<Thread(Thread-6, started 4752)> 100
<Thread(Thread-10, started 9796)> 100
<Thread(Thread-5, started 10552)> 100
<Thread(Thread-9, started 10480)> 100
可以看到,結(jié)果顯示和使用局部變量的結(jié)果一樣溉箕。
再看一個(gè)threading.local的例子:
# threading.local例子
import threading
X = 'abc'
ctx = threading.local() # 注意這個(gè)對象所處的線程
ctx.x = 123
print(ctx, type(ctx), ctx.x)
def worker():
print(X)
print(ctx)
print(ctx.x)
print('working')
worker() # 普通函數(shù)調(diào)用
print()
threading.Thread(target=worker).start() # 另起一個(gè)線程
# 打印結(jié)果
<_thread._local object at 0x000000000299E570> <class '_thread._local'> 123
abc
<_thread._local object at 0x000000000299E570>
123
working
abc
<_thread._local object at 0x000000000299E570>
Exception in thread Thread-1:
Traceback (most recent call last):
File "C:\Users\pc\AppData\Local\Programs\Python\Python36\lib\threading.py", line 916, in _bootstrap_inner
self.run()
File "C:\Users\pc\AppData\Local\Programs\Python\Python36\lib\threading.py", line 864, in run
self._target(*self._args, **self._kwargs)
File "F:/PycharmProjects/0524_線程進(jìn)程.py", line 127, in worker
print(ctx.x)
AttributeError: '_thread._local' object has no attribute 'x'
從運(yùn)行結(jié)果來看晦墙,另起一個(gè)線程打印ctx.x出錯(cuò)了:
AttributeError: '_thread._local' object has no attribute 'x'
但是,ctx打印沒有出錯(cuò)肴茄,說明能看到ctx晌畅,但是ctx中的x看不到,這個(gè)x不能跨線程寡痰。
threading.local類構(gòu)建了一個(gè)大字典抗楔,其元素是每一線程實(shí)例的地址為key和線程對象引用線程單獨(dú)的字典的映射,如下:
{ id(Thread) -> (ref(Thread), thread-local dict) }
通過threading.local實(shí)例就可在不同的線程中拦坠,安全地使用線程獨(dú)有的數(shù)據(jù)连躏,做到了線程間數(shù)據(jù)隔離,如同本地變量一樣安全贞滨。
定時(shí)器Timer/延遲執(zhí)行
threading.Timer繼承自Thread入热,這個(gè)類用來定義多久執(zhí)行一個(gè)函數(shù)。
class threading.Timer(interval, function, args=None, kwargs=None)
start方法執(zhí)行之后晓铆,Timer對象會(huì)處于等待狀態(tài)勺良,等待了interval之后,開始執(zhí)行function函數(shù)的骄噪。如果在執(zhí)行函數(shù)之前的等待階段尚困,使用了cancel方法,就會(huì)跳過執(zhí)行函數(shù)結(jié)束链蕊。
import threading
import logging
import time
FORMAT = '%(asctime)s %(threadName)s %(thread)d %(message)s'
logging.basicConfig(format=FORMAT, level=logging.INFO)
def worker():
logging.info('in worker')
time.sleep(2)
t = threading.Timer(5, worker)
t.setName('w1')
t.start() # 啟動(dòng)線程
print(threading.enumerate())
# 打印結(jié)果:
[<_MainThread(MainThread, started 7360)>, <Timer(w1, started 6428)>]
2018-05-27 15:40:13,706 w1 8052 in worker
在上面的基礎(chǔ)上加入t.cancel()后:
t.cancel()
time.sleep(1)
print(threading.enumerate())
# 打印結(jié)果
[<_MainThread(MainThread, started 6660)>, <Timer(w1, started 7284)>]
[<_MainThread(MainThread, started 6660)>]
如果線程中worker函數(shù)已經(jīng)開始執(zhí)行尾组,cancel就沒有任何效果了忙芒。
總結(jié):
Timer是線程Thread的子類,就是線程類讳侨,具有線程的能力和特征。
它的實(shí)例是能夠延時(shí)執(zhí)行目標(biāo)函數(shù)的線程奏属,在真正執(zhí)行目標(biāo)函數(shù)之前跨跨,都可以cancel它。
提前cancel:
import threading
import logging
import time
FORMAT = '%(asctime)s %(threadName)s %(thread)d %(message)s'
logging.basicConfig(format=FORMAT, level=logging.INFO)
def worker():
logging.info('in worker')
time.sleep(2)
t = threading.Timer(5, worker)
t.setName('w1')
t.cancel() # 提前取消
t.start() # 啟動(dòng)線程
print(threading.enumerate())
time.sleep(3)
print(threading.enumerate())
# 打印結(jié)果
[<_MainThread(MainThread, started 6684)>]
[<_MainThread(MainThread, started 6684)>]