4 多線程
gitbook鏈接:用python帶你進(jìn)入AI中的深度學(xué)習(xí)技術(shù)領(lǐng)域https://www.gitbook.com/book/scrappyzhang/python_to_deeplearn/details
github鏈接:https://github.com/ScrappyZhang/python_web_Crawler_DA_ML_DL
在我們UDP那一章漂佩,聊天器實(shí)例的發(fā)送消息和接收消息是同步的轴脐,然而QQ可以同時(shí)收發(fā)消息耶,為什么呢爆捞?如何通過單進(jìn)程實(shí)現(xiàn)呢鲤脏?這就是接下來學(xué)習(xí)的動(dòng)力芬骄。
4.1 多線程概念引入
在現(xiàn)實(shí)生活中很多事情都是同時(shí)進(jìn)行的寒矿,比如開車時(shí)我們的手握著方向盤、腳踩著檔位以實(shí)現(xiàn)駕駛汽車的一些操作太援,這兩個(gè)動(dòng)作是同時(shí)進(jìn)行的闽晦。除此之外就像唱歌跳舞那樣,很多演唱會(huì)的明星都是在唱歌時(shí)提岔,舞臺(tái)上伴隨著舞蹈。想象一下笋敞,如果一個(gè)很一般的歌手碱蒙,自己先向大家唱完歌,然后不會(huì)跳舞的他再給大家跳一段屬于那首歌的舞蹈夯巷,那這個(gè)演唱會(huì)就太失敗了赛惩,除非他是杰克遜。接下來我們用程序模擬下唱歌跳舞趁餐。
'''net03_sing_dance.py'''
import time
def sing():
for i in range(5):
print('正在唱歌呢 %d' % i)
time.sleep(1) # 休息1秒
def dance():
for i in range(5):
print('正在跳舞呢 %d' % i)
time.sleep(1) # 休息1秒
if __name__ == '__main__':
sing() # 唱歌
dance() # 跳舞
這段代碼中喷兼,我們定義了一個(gè)唱歌函數(shù)和一個(gè)跳舞函數(shù),里面用print語句模擬正在進(jìn)行的狀態(tài)后雷,為了更好的再后續(xù)分析季惯,我們讓其每次在中間休息1s中吠各。
從執(zhí)行結(jié)果來看,唱完歌之后才會(huì)去跳舞勉抓。為什么呢贾漏?就像下圖左邊那樣,我們的程序串行的執(zhí)行了所有任務(wù)藕筋;而生活中往往需要我們同時(shí)處理兩個(gè)任務(wù)纵散,就像下圖右邊那樣,即多任務(wù)處理隐圾。多任務(wù)處理中有種叫做線程的東西可以幫我們實(shí)現(xiàn)這樣的想法伍掀。
請(qǐng)看定義:線程是程序中一個(gè)單一的順序控制流程。進(jìn)程內(nèi)有一個(gè)相對(duì)獨(dú)立的暇藏、可調(diào)度的執(zhí)行單元蜜笤,是系統(tǒng)獨(dú)立調(diào)度和分派CPU的基本單位指令運(yùn)行時(shí)的程序的調(diào)度單位。在單個(gè)程序中同時(shí)運(yùn)行多個(gè)線程完成不同的工作叨咖,稱為多線程瘩例。
從定義中可以看出我們之前的那段代碼是一個(gè)單一的順序控制流程,即單線程程序甸各。為了實(shí)現(xiàn)同時(shí)唱歌和跳舞垛贤,若采用線程實(shí)現(xiàn),我們必須編寫多線程程序趣倾。請(qǐng)看下面這段代碼聘惦。
'''net03_sing_dance_threading.py'''
import time
import threading
def sing():
for i in range(5):
print('正在唱歌呢 %d' % i)
time.sleep(1) # 休息1秒
def dance():
for i in range(5):
print('正在跳舞呢 %d' % i)
time.sleep(1) # 休息1秒
if __name__ == '__main__':
td1 = threading.Thread(target=sing) # 創(chuàng)建唱歌子線程
td2 = threading.Thread(target=dance) # 創(chuàng)建跳舞子線程
td1.start() # 開始運(yùn)行子線程
td2.start() # 開始運(yùn)行子線程
從執(zhí)行結(jié)果可以很清晰的看到唱歌和跳舞似乎是在同時(shí)交替進(jìn)行,實(shí)現(xiàn)了我們的目的儒恋。為什么會(huì)這樣呢善绎?我們先不管python中的語法,先來清晰的了解下多線程的運(yùn)行機(jī)制诫尽。
首先我們先了解下線程的幾個(gè)狀態(tài)禀酱。線程有就緒、阻塞和運(yùn)行三種基本狀態(tài)牧嫉。就緒狀態(tài)是指線程具備運(yùn)行的所有條件剂跟,邏輯上可以運(yùn)行,在等待處理機(jī)酣藻;運(yùn)行狀態(tài)是指線程占有處理機(jī)正在運(yùn)行曹洽;阻塞狀態(tài)是指線程在等待一個(gè)事件(如某個(gè)信號(hào)量),邏輯上不可執(zhí)行辽剧。在我們上面的那段程序中送淆,可以看到唱歌在執(zhí)行時(shí),每次都會(huì)休息1S怕轿,即唱歌線程從運(yùn)行態(tài)進(jìn)入了阻塞態(tài)偷崩;此時(shí)系統(tǒng)空閑下來了辟拷,多線程機(jī)制將空閑的資源用來執(zhí)行跳舞;當(dāng)跳舞又休息時(shí)环凿,又返回來執(zhí)行不休息的唱歌梧兼。就像下圖所示那樣。由于CPU處理的時(shí)間非持翘快羽杰,我們會(huì)感知上以為唱歌和跳舞是在同時(shí)運(yùn)行。這就是多線程的基本流程到推,這樣可以充分利用一些阻塞狀態(tài)來實(shí)現(xiàn)資源的充分利用考赛,提高效率。
注:每一個(gè)程序都至少有一個(gè)線程莉测,若程序只有一個(gè)線程颜骤,那就是程序本身。
4.2 線程講解
上一節(jié)我們通過唱歌和跳舞的實(shí)例向大家展示了線程的概念和魅力捣卤。本節(jié)我們具體說明下線程的由來忍抽、特點(diǎn)、適用范圍等董朝。
由來:
上世紀(jì)60年代鸠项,在操作系統(tǒng)中能擁有資源和獨(dú)立運(yùn)行的基本單位是進(jìn)程,然而隨著計(jì)算機(jī)技術(shù)的發(fā)展子姜,進(jìn)程出現(xiàn)了很多弊端祟绊,一是由于進(jìn)程是資源擁有者,創(chuàng)建哥捕、撤消與切換存在較大的時(shí)空開銷牧抽,因此需要引入輕型進(jìn)程;二是由于對(duì)稱多處理機(jī)(SMP)出現(xiàn)遥赚,可以滿足多個(gè)運(yùn)行單位扬舒,而多個(gè)進(jìn)程并行開銷過大。因此在80年代凫佛,出現(xiàn)了能獨(dú)立運(yùn)行的基本單位——線程(Threads)呼巴。
特性:
在多線程操作系統(tǒng)中,通常是在一個(gè)進(jìn)程中包括多個(gè)線程御蒲,每個(gè)線程都是作為利用CPU的基本單位,是花費(fèi)最小開銷的實(shí)體诊赊。線程具有以下屬性厚满。
1)輕型實(shí)體
線程中的實(shí)體基本上不擁有系統(tǒng)資源,只是有一點(diǎn)必不可少的碧磅、能保證獨(dú)立運(yùn)行的資源碘箍。線程的實(shí)體包括程序遵馆、數(shù)據(jù)和TCB。線程是動(dòng)態(tài)概念丰榴,它的動(dòng)態(tài)特性由線程控制塊TCB(Thread Control Block)描述货邓。TCB包括以下信息:
(1)線程狀態(tài)。
(2)當(dāng)線程不運(yùn)行時(shí)四濒,被保存的現(xiàn)場資源换况。
(3)一組執(zhí)行堆棧。
(4)存放每個(gè)線程的局部變量主存區(qū)盗蟆。
(5)訪問同一個(gè)進(jìn)程中的主存和其它資源戈二。
用于指示被執(zhí)行指令序列的程序計(jì)數(shù)器、保留局部變量喳资、少數(shù)狀態(tài)參數(shù)和返回地址等的一組寄存器和堆棧觉吭。
2)獨(dú)立調(diào)度和分派的基本單位。
在多線程操作系統(tǒng)中仆邓,線程是能獨(dú)立運(yùn)行的基本單位鲜滩,因而也是獨(dú)立調(diào)度和分派的基本單位。由于線程很“輕”节值,故線程的切換非常迅速且開銷嗅愎琛(在同一進(jìn)程中的)。
3)可并發(fā)執(zhí)行察署。
在一個(gè)進(jìn)程中的多個(gè)線程之間闷游,可以并發(fā)執(zhí)行,甚至允許在一個(gè)進(jìn)程中所有線程都能并發(fā)執(zhí)行贴汪;同樣脐往,不同進(jìn)程中的線程也能并發(fā)執(zhí)行,充分利用和發(fā)揮了處理機(jī)與外圍設(shè)備并行工作的能力扳埂。
4)共享進(jìn)程資源业簿。
在同一進(jìn)程中的各個(gè)線程,都可以共享該進(jìn)程所擁有的資源阳懂,這首先表現(xiàn)在:所有線程都具有相同的地址空間(進(jìn)程的地址空間)梅尤,這意味著,線程可以訪問該地址空間的每一個(gè)虛地址岩调;此外巷燥,還可以訪問進(jìn)程所擁有的已打開文件、定時(shí)器号枕、信號(hào)量機(jī)構(gòu)等缰揪。由于同一個(gè)進(jìn)程內(nèi)的線程共享內(nèi)存和文件,所以線程之間互相通信不必調(diào)用內(nèi)核葱淳。
與進(jìn)程的比較(內(nèi)容在下一章):
進(jìn)程是資源分配的基本單位钝腺。所有與該進(jìn)程有關(guān)的資源抛姑,都被記錄在進(jìn)程控制塊PCB中。以表示該進(jìn)程擁有這些資源或正在使用它們艳狐。另外定硝,進(jìn)程也是搶占處理機(jī)的調(diào)度單位,它擁有一個(gè)完整的虛擬地址空間毫目。當(dāng)進(jìn)程發(fā)生調(diào)度時(shí)蔬啡,不同的進(jìn)程擁有不同的虛擬地址空間,而同一進(jìn)程內(nèi)的不同線程共享同一地址空間蒜茴。
與進(jìn)程相對(duì)應(yīng)星爪,線程與資源分配無關(guān),它屬于某一個(gè)進(jìn)程粉私,并與進(jìn)程內(nèi)的其他線程一起共享進(jìn)程的資源顽腾。
線程只由相關(guān)堆棧(系統(tǒng)棧或用戶棧)寄存器和線程控制表TCB組成诺核。寄存器可被用來存儲(chǔ)線程內(nèi)的局部變量抄肖,但不能存儲(chǔ)其他線程的相關(guān)變量。
通常在一個(gè)進(jìn)程中可以包含若干個(gè)線程窖杀,它們可以利用進(jìn)程所擁有的資源漓摩。在引入線程的操作系統(tǒng)中,通常都是把進(jìn)程作為分配資源的基本單位入客,而把線程作為獨(dú)立運(yùn)行和獨(dú)立調(diào)度的基本單位管毙。由于線程比進(jìn)程更小,基本上不擁有系統(tǒng)資源桌硫,故對(duì)它的調(diào)度所付出的開銷就會(huì)小得多夭咬,能更高效的提高系統(tǒng)內(nèi)多個(gè)程序間并發(fā)執(zhí)行的程度,從而顯著提高系統(tǒng)資源的利用率和吞吐量铆隘。因而近年來推出的通用操作系統(tǒng)都引入了線程卓舵,以便進(jìn)一步提高系統(tǒng)的并發(fā)性,并把它視為現(xiàn)代操作系統(tǒng)的一個(gè)重要指標(biāo)膀钠。
線程與進(jìn)程的區(qū)別可以歸納為以下4點(diǎn):
1)地址空間和其它資源(如打開文件):進(jìn)程間相互獨(dú)立掏湾,同一進(jìn)程的各線程間共享。某進(jìn)程內(nèi)的線程在其它進(jìn)程不可見肿嘲。
2)通信:進(jìn)程間通信IPC融击,線程間可以直接讀寫進(jìn)程數(shù)據(jù)段(如全局變量)來進(jìn)行通信——需要進(jìn)程同步和互斥手段的輔助,以保證數(shù)據(jù)的一致性雳窟。
3)調(diào)度和切換:線程上下文切換比進(jìn)程上下文切換要快得多砚嘴。
4)在多線程操作系統(tǒng)中,進(jìn)程不是一個(gè)可執(zhí)行的實(shí)體。
4.3 在python中使用線程
python中最常用的線程模塊為threading际长,可以完成多線程編寫的大多數(shù)任務(wù)。目前除了個(gè)別UNIX不支持多線程外兴泥,基本上所有的python流行平臺(tái)都支持線程了工育。接下來我們對(duì)照上一節(jié)那個(gè)同時(shí)唱歌跳舞的代碼net03_sing_dance_threading.py
。
我們在使用threading時(shí)搓彻,最經(jīng)典的方式是:一般需要先導(dǎo)入該模塊如绸;再通過threading中的Thread創(chuàng)建一個(gè)Thread對(duì)象;然后啟動(dòng)該線程start即可旭贬。
import threading
td1 = threading.Thread(target=sing) # 創(chuàng)建唱歌子線程
td1.start() # 開始運(yùn)行子線程
其中怔接,Thread類的最常用語法如下:
threading.Thread(target=None, name=None, args=(), kwargs={})
- target賦值為要被調(diào)用的子進(jìn)程對(duì)象
- name為將該進(jìn)程自定義一個(gè)標(biāo)識(shí)名稱
- args為調(diào)用時(shí)傳入的無名參數(shù)
- kwargs為調(diào)用時(shí)傳入的有名參數(shù)
在threading中提供了查詢當(dāng)前進(jìn)程中還運(yùn)行的線程函數(shù)enumerate()和當(dāng)前運(yùn)行線程數(shù)量的函數(shù)active_count(),我們向net03_sing_dance_threading.py添加進(jìn)如下代碼稀轨。
'''net03_threading_enumerate.py'''
while True:
length = len(threading.enumerate()) # 當(dāng)前線程數(shù)量
print('通過active_count查詢到的線程數(shù):', threading.active_count()) # 當(dāng)前線程的數(shù)量
print(threading.enumerate()) # 打印顯示目前還存在的線程
print('當(dāng)前運(yùn)行的線程數(shù)為:%d' % length)
if length <= 1: # 除了兩個(gè)子進(jìn)程扼脐,還有默認(rèn)的父進(jìn)程,所以當(dāng)唱歌跳舞執(zhí)行完畢后奋刽,還剩一個(gè)線程
break
time.sleep(0.5)
可以很清楚的看到瓦侮,運(yùn)行中總共有三個(gè)線程:
默認(rèn)父線程:<_MainThread(MainThread, started 140735499973440)>
子線程1:<Thread(Thread-1, started 123145313566720)>
子線程2:<Thread(Thread-2, started 123145318821888)>
當(dāng)所有子線程結(jié)束時(shí),才會(huì)結(jié)束父線程佣谐。
4.4 自定義Thread子類
上一節(jié)我們演示了創(chuàng)建Thread對(duì)象的基本方法肚吏。但是那是基于過程的一種思想,我們python是一種面向?qū)ο蟮恼Z言狭魂,常常在開發(fā)中往往是將相關(guān)功能封裝好進(jìn)行調(diào)用罚攀,因此我們常常需要?jiǎng)?chuàng)建一個(gè)自定義Thread子類。
5.4.1 Thread類常用方法
start()啟動(dòng)線程活動(dòng)
每個(gè)子線程如果需要啟動(dòng)運(yùn)行雌澄,必須運(yùn)行該方法斋泄,否則不執(zhí)行。即之前的例子中創(chuàng)建Thread對(duì)象后子線程并沒有執(zhí)行掷伙,而是執(zhí)行到start()時(shí)才會(huì)啟動(dòng)是己。每個(gè)子進(jìn)程此方法只能調(diào)用一次,否則會(huì)報(bào)錯(cuò)(假如我們連續(xù)寫兩行start()調(diào)用啟動(dòng)同一個(gè)Thread對(duì)象則會(huì)報(bào)錯(cuò)RuntimeError)任柜。
run() 線程處理方法
該方法主要是執(zhí)行線程要處理的內(nèi)容卒废。默認(rèn)的Thread類run方法部分源代碼如下
if self._target:
self._target(*self._args, **self._kwargs)
即調(diào)用用戶賦予的target函數(shù)對(duì)象,例如我們之前的唱歌跳舞函數(shù)宙地。因此摔认,我們常常在自定義Thread子類時(shí),均是重載此方法來實(shí)現(xiàn)自定義的線程處理內(nèi)容的宅粥。
join(timeout=None)
默認(rèn)為程序在此等待直到線程終止参袱。一般也可賦值,如join(timeout=3)為等待3秒后,不管該子線程是否執(zhí)行完畢均執(zhí)行接下來的父線程內(nèi)容抹蚀。
4.4.2 自定義Thread子類
根據(jù)上一節(jié)的知識(shí)剿牺,我們已經(jīng)知道,自定義Thread子類需要重載run方法环壤。假如我們現(xiàn)在要以面向?qū)ο蟮男问街匦戮帉懼暗某杼鑼?shí)例晒来,該怎么做呢?那就是定義Thread子類郑现,如下面這段偽代碼:
class sing(threading.Thread):
def run(self):
# do something
pass
現(xiàn)在我們重寫唱歌跳舞實(shí)例湃崩,完整的代碼如下:
'''net03_Thread_subclass.py'''
import threading
import time
class Sing(threading.Thread):
def run(self):
for i in range(5):
print('正在唱歌呢 %d' % i)
time.sleep(1) # 休息1秒
class Dance(threading.Thread):
def run(self):
for i in range(5):
print('正在跳舞呢 %d' % i)
time.sleep(1) # 休息1秒
if __name__ == '__main__':
my_sing = Sing()
my_dance = Dance()
my_sing.start()
my_dance.start()
結(jié)果和之前是一樣的,但是代碼構(gòu)造更簡潔了接箫。
4.5 線程間共享全局變量
4.5.1 子線程的全局變量修改結(jié)果共享到其他線程中
之前我們講過線程的一個(gè)特性就是共享全局變量攒读,這里我們通過一個(gè)代碼實(shí)例進(jìn)行演示,依舊是之前的唱歌跳舞實(shí)例,僅僅是在唱歌時(shí)對(duì)全局變量global_num進(jìn)行修改辛友,可以看到跳舞子進(jìn)程和父進(jìn)程在讀取全局變量global_num時(shí)薄扁,已經(jīng)發(fā)生了變化,從最初的0變?yōu)榱? 瞎领。這就是線程間全局變量共享泌辫。
'''net03_global_variables.py'''
import threading
import time
global_num = 0 # 全局變量
class Sing(threading.Thread):
def run(self):
for i in range(3):
print('正在唱歌呢 %d' % i)
global global_num
global_num = global_num + i # 修改全局變量
time.sleep(1) # 休息1秒
print('全局變量sing global_num= ', global_num)
class Dance(threading.Thread):
def run(self):
for i in range(3):
print('正在跳舞呢 %d' % i)
time.sleep(1) # 休息1秒
global global_num
print('全局變量dance global_num= ', global_num)
if __name__ == '__main__':
my_sing = Sing()
my_dance = Dance()
my_sing.start()
my_dance.start()
my_sing.join() # 待子進(jìn)程結(jié)束后再向下執(zhí)行
my_dance.join() # 待子進(jìn)程結(jié)束后再向下執(zhí)行
print('全局變量main global_num= ', global_num)
4.5.2 線程間全局變量競爭
上一節(jié)我們看到了線程間可以共享全局變量,但這并不是一件好事情九默。假如各個(gè)子線程對(duì)全局變量都在修改震放,由于子線程的執(zhí)行是由各子線程的實(shí)際狀態(tài)進(jìn)行交替運(yùn)行的,并不能控制何時(shí)何地修改到何種程度驼修,就會(huì)造成全局變量的混亂殿遂,得不到開發(fā)者想要獲取的結(jié)果,這叫非線程安全或者全局資源競爭乙各。我們通過修改上一節(jié)的代碼來演示墨礁,這次我們在唱歌和跳舞兩個(gè)子進(jìn)程里面均對(duì)全局變量global_num加1循環(huán)1000萬次。如果不考慮資源競爭耳峦,應(yīng)該會(huì)得到最終結(jié)尾哦為2000W恩静。**實(shí)際結(jié)果呢?僅僅為12893573 **蹲坷。這就是資源競爭驶乾,并且具體競爭的狀態(tài)開發(fā)者并不知道,導(dǎo)致變量結(jié)果混亂循签,每次運(yùn)行結(jié)果都不一樣级乐。
'''net03_global_variable_competition'''
import threading
global_num = 0 # 全局變量
class Sing(threading.Thread):
def run(self):
global global_num
print('開始:全局變量sing global_num= ', global_num)
for i in range(10000000):
global_num = global_num + 1 # 修改全局變量
print('結(jié)束:全局變量sing global_num= ', global_num)
class Dance(threading.Thread):
def run(self):
global global_num
print('開始:全局變量dance global_num= ', global_num)
for i in range(10000000):
global_num = global_num + 1 # 修改全局變量
print('結(jié)束:全局變量dance global_num= ', global_num)
if __name__ == '__main__':
print('開始:全局變量main global_num= ', global_num)
my_sing = Sing()
my_dance = Dance()
my_sing.start()
my_dance.start()
my_sing.join() # 待子進(jìn)程結(jié)束后再向下執(zhí)行
my_dance.join() # 待子進(jìn)程結(jié)束后再向下執(zhí)行
print('結(jié)束:全局變量main global_num= ', global_num)
如何解決這個(gè)問題呢?我們來思考一下:只有明確的知道何時(shí)Sing修改global_num并結(jié)束修改县匠、Dance修改global_num并結(jié)束修改风科,我們才能精準(zhǔn)地使global_num獲取到開發(fā)者想要賦予的值撒轮。這就叫線程間同步。
同步就是協(xié)同步調(diào)贼穆,按預(yù)定的先后次序進(jìn)行運(yùn)行题山。如:你說完,我再說扮惦。"同"字從字面上容易理解為一起動(dòng)作臀蛛,其實(shí)不是,"同"字應(yīng)是指協(xié)同崖蜜、協(xié)助、互相配合客峭。如進(jìn)程豫领、線程同步,可理解為進(jìn)程或線程A和B一塊配合舔琅,A執(zhí)行到一定程度時(shí)要依靠B的某個(gè)結(jié)果等恐,于是停下來,示意B運(yùn)行;B執(zhí)行备蚓,再將結(jié)果給A;A再繼續(xù)操作课蔬。
所以解決線程同時(shí)修改全局變量的方式的方法就是:對(duì)于上一小節(jié)提出的那個(gè)計(jì)算錯(cuò)誤的問題,可以通過線程同步來進(jìn)行解決郊尝。思路二跋,如下:
- 系統(tǒng)調(diào)用Sing,然后獲取到global_num的值為0流昏,此時(shí)上一把鎖扎即,即不允許其他線程操作global_num
- Sing對(duì)global_num的值進(jìn)行+1操作
- Sing解鎖,其他的線程就可以使用g_num了况凉,而且是g_num的值不是0而是修改后的值
- 同理其他線程在對(duì)global_num進(jìn)行修改時(shí)谚鄙,都要先上鎖,處理完后再解鎖刁绒,在上鎖的整個(gè)過程中不允許其他線程訪問闷营,就保證了數(shù)據(jù)的正確性。
4.6 互斥鎖
4.6.1 python互斥鎖使用
上一節(jié)我們提到解決全局變量資源競爭的有效方式是線程間實(shí)現(xiàn)同步知市,而實(shí)現(xiàn)線程間同步時(shí)候需要一把鎖來處理全局變量傻盟。在python中該如何做呢?
線程同步能夠保證多個(gè)線程安全訪問競爭資源初狰,最簡單的同步機(jī)制是引入互斥鎖莫杈。互斥鎖為資源引入一個(gè)狀態(tài):鎖定/非鎖定奢入。某個(gè)線程要更改共享數(shù)據(jù)時(shí)筝闹,先將其鎖定媳叨,此時(shí)資源的狀態(tài)為“鎖定”,其他線程不能更改关顷;直到該線程釋放資源糊秆,將資源的狀態(tài)變成“非鎖定”,其他的線程才能再次鎖定該資源议双《环互斥鎖保證了每次只有一個(gè)線程進(jìn)行寫入操作,從而保證了多線程情況下數(shù)據(jù)的正確性平痰。
hreading模塊中定義了Lock類汞舱,可以方便的處理鎖定:
# 創(chuàng)建鎖
mutex = threading.Lock()
# 鎖定
mutex.acquire()
# 釋放
mutex.release()
將上一節(jié)對(duì)全局變量global_num同時(shí)相加1000萬次的代碼在修改操作位置進(jìn)行如下修改:增加鎖、獲取鎖和釋放鎖宗雇。從執(zhí)行結(jié)果中可以看到昂芜,全局變量操作沒有被競爭打亂。
'''net03_mutex_lock.py'''
# 主程序中增加互斥鎖
if __name__ == '__main__':
mutex = threading.Lock()
# 自定義Thread子類修改全局變量前獲取鎖赔蒲,修改后釋放鎖
for i in range(10000000):
mutex.acquire() # 鎖定全局變量
global_num = global_num + 1
mutex.release() # 釋放全局變量
4.6.2 互斥鎖特性
互斥鎖要點(diǎn):
如果一個(gè)鎖之前是沒有上鎖的泌神,那么acquire不會(huì)堵塞。
如果在調(diào)用acquire對(duì)這個(gè)鎖上鎖之前 它已經(jīng)被 其他線程上了鎖舞虱,那么此時(shí)acquire會(huì)堵塞欢际,直到這個(gè)鎖被解鎖為止。
上鎖解鎖過程:
當(dāng)一個(gè)線程調(diào)用鎖的acquire()方法獲得鎖時(shí)矾兜,鎖就進(jìn)入“l(fā)ocked”狀態(tài)损趋。
每次只有一個(gè)線程可以獲得鎖。如果此時(shí)另一個(gè)線程試圖獲得這個(gè)鎖焕刮,該線程就會(huì)變?yōu)椤癰locked”狀態(tài)舶沿,稱為“阻塞”,直到擁有鎖的線程調(diào)用鎖的release()方法釋放鎖之后配并,鎖進(jìn)入“unlocked”狀態(tài)括荡。
線程調(diào)度程序從處于同步阻塞狀態(tài)的線程中選擇一個(gè)來獲得鎖,并使得該線程進(jìn)入運(yùn)行(running)狀態(tài)溉旋。
總結(jié)
鎖的好處:確保了某段關(guān)鍵代碼只能由一個(gè)線程從頭到尾完整地執(zhí)行畸冲。
鎖的壞處:阻止了多線程并發(fā)執(zhí)行,包含鎖的某段代碼實(shí)際上只能以單線程模式執(zhí)行观腊,效率就大大地下降了邑闲。由于可以存在多個(gè)鎖,不同的線程持有不同的鎖梧油,并試圖獲取對(duì)方持有的鎖時(shí)苫耸,可能會(huì)造成死鎖。
4.6.3 死鎖
在線程間共享多個(gè)資源的時(shí)候儡陨,如果兩個(gè)線程分別占有一部分資源并且同時(shí)等待對(duì)方的資源褪子,就會(huì)造成死鎖量淌。盡管死鎖很少發(fā)生,但一旦發(fā)生就會(huì)造成應(yīng)用的停止響應(yīng)∠油剩現(xiàn)實(shí)社會(huì)中呀枢,談判雙方態(tài)度均強(qiáng)硬并且都在等待對(duì)方低頭,如果雙方都這樣固執(zhí)的等待對(duì)方先低頭笼痛,弄不好談判就崩潰咯裙秋。
我們修改上一節(jié)的帶有互斥鎖的全局變量同時(shí)修改操作程序,注釋取消掉Sing類里的釋放鎖語句缨伊,然后運(yùn)行程序摘刑。我們會(huì)發(fā)現(xiàn)程序卡住啦。這就是死鎖現(xiàn)象刻坊。
'''net03_dead_lock.py'''
class Sing(threading.Thread):
def run(self):
global global_num
print('開始:全局變量sing global_num= ', global_num)
for i in range(10000000):
mutex.acquire() # 鎖定全局變量
global_num = global_num + 1
# mutex.release() # 釋放全局變量
print('結(jié)束:全局變量sing global_num= ', global_num)
常見的死鎖原因有開發(fā)設(shè)計(jì)時(shí)未適當(dāng)釋放鎖(比如此例)泣侮、鎖太多相互嵌套造成所資源未釋放。
常見的避免死鎖方法有:程序設(shè)計(jì)時(shí)要盡量避免(銀行家算法紧唱、、使用上下文管理(with lock)保證鎖的成對(duì)獲取釋放)隶校,添加超時(shí)時(shí)間等漏益。
4.7 多線程UDP聊天器
需求實(shí)現(xiàn):
通過多線程的方式實(shí)現(xiàn)這樣一個(gè)UDP聊天器:在發(fā)送數(shù)據(jù)的同時(shí)也能夠自動(dòng)接收數(shù)據(jù)。
根據(jù)流程圖書寫代碼
- 一個(gè)發(fā)送數(shù)據(jù)函數(shù)
def send_message():
- 一個(gè)接收數(shù)據(jù)函數(shù)
def recv_message(udp_sock):
- 父進(jìn)程創(chuàng)建套接字綁定端口后調(diào)用發(fā)送數(shù)據(jù)函數(shù)
send_message()
- 子進(jìn)程調(diào)用接收數(shù)據(jù)函數(shù)實(shí)現(xiàn)接收數(shù)據(jù)功能
td1 = threading.Thread(target=recv_message, args=(sock, ))
完整代碼
'''net03_udp_threading_chat.py'''
import threading
import socket
def send_message():
send_ip = input('請(qǐng)輸入要發(fā)送的ip:\n')
send_port = input('請(qǐng)輸入要發(fā)送的端口:\n')
send_address = (send_ip, int(send_port))
while True:
send_data = input('請(qǐng)輸入要發(fā)送的消息:\n')
sock.sendto(send_data.encode('utf-8'), send_address)
def recv_message(udp_sock):
while True:
recv_data = udp_sock.recvfrom(1024) # 接收數(shù)據(jù)
print('從', recv_data[1], '接收的數(shù)據(jù)為:', recv_data[0].decode('utf-8'))
if __name__ == '__main__':
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
address = ('localhost', 8888) # 地址:設(shè)定服務(wù)器要使用端口8888
sock.bind(address) # 綁定端口
td1 = threading.Thread(target=recv_message, args=(sock, ))
td1.start()
send_message()
實(shí)現(xiàn)結(jié)果
我們此例中深胳,設(shè)置聊天器的ip為本地localhost(127.0.0.1)绰疤、端口8888;本地的網(wǎng)絡(luò)助手UDP端口設(shè)置為8080 舞终。運(yùn)行聊天器和網(wǎng)絡(luò)助手后轻庆,我們先通過聊天器向網(wǎng)絡(luò)助手發(fā)送了兩次的hello udp,然后在輸入第三次的消息hello udp threading時(shí)敛劝,從網(wǎng)絡(luò)助手向聊天器發(fā)送了Hello threading余爆。可以看到聊天器成功的在發(fā)送數(shù)據(jù)輸入時(shí)接收到了網(wǎng)絡(luò)助手發(fā)來的消息夸盟。最后我們用網(wǎng)絡(luò)助手再次發(fā)行了Hello threading蛾方,聊天器依舊成功接收到,并運(yùn)行等待在輸入界面上陕。這樣我們便通過多線程的方式實(shí)現(xiàn)了同時(shí)收發(fā)數(shù)據(jù)的UDP聊天器桩砰。