一直懶得寫Python相關(guān)的文章,恰好有天需要簡(jiǎn)單的給童鞋們講點(diǎn)課,倉促之余就誕生了此文.
今天本來準(zhǔn)備全面的聊聊有關(guān)高性能并發(fā)這個(gè)話題來著,但是周末馬上要來了啊.所以我就取了其中的一點(diǎn)來介紹,關(guān)于其他的方面,有興趣的小伙伴可以和我交流.談高效并發(fā),往往脫離不了以下三種方案:
- 進(jìn)程:每個(gè)邏輯控制流都是一個(gè)進(jìn)程,由內(nèi)核來調(diào)度和維護(hù).因?yàn)檫M(jìn)程有獨(dú)立的虛擬地址空間,想要和其他控制流通信必須依靠顯示的進(jìn)程間通信,即我們所說的IPC機(jī)制
- 線程:線程應(yīng)該是我們最為熟知的.它本質(zhì)是運(yùn)行在一個(gè)單一進(jìn)程上下文中的邏輯流,由內(nèi)核進(jìn)行調(diào)度.
- I/O多路復(fù)用:應(yīng)用程序在一個(gè)進(jìn)程的上下文中顯式地調(diào)度他們自己的邏輯流.邏輯流被模型化為狀態(tài)機(jī),數(shù)據(jù)到達(dá)文件描述符之后,主程序顯式地從一個(gè)狀態(tài)轉(zhuǎn)換為另一個(gè)狀態(tài).因?yàn)槌绦蚨际且砸粋€(gè)單獨(dú)的進(jìn)程,所以所有的流都共享同一個(gè)地址空間.基本的思路就是使用select函數(shù)要求內(nèi)核掛起進(jìn)程,只有一個(gè)或多個(gè)I/O事件發(fā)生后,才將控制權(quán)返回給應(yīng)用程序
看起來令人難以理解,但幸運(yùn)的是Python中針對(duì)這三方面都提供了響應(yīng)的支持,簡(jiǎn)化了我們的操作.那今天咱就聊聊其中的一點(diǎn)--線程.為什么選擇線程呢?一方面考慮到大部分人都有線程這個(gè)概念,另一方面考慮相比進(jìn)程線程更輕量級(jí),相比協(xié)程,線程更易于理解.進(jìn)程和線程之間的關(guān)系可以用衣服最簡(jiǎn)單的圖來表示:
線程的狀態(tài)
任何一門支持線程的語言都可以具備以下幾種運(yùn)行狀態(tài),無論是你做Java,Python還是C,首先來看下面一張圖:
在這里我簡(jiǎn)單來解釋以下這幾種狀態(tài)的含義:
- 新建:使用線程的第一步就是創(chuàng)建線程,創(chuàng)建后的線程只是進(jìn)入可執(zhí)行的狀態(tài),也就是Runnable
- Runnable:進(jìn)入此狀態(tài)的線程還并未開始運(yùn)行,一旦CPU分配時(shí)間片給這個(gè)線程后,該線程才正式的開始運(yùn)行
- Running:線程正式開始運(yùn)行,在運(yùn)行過程中線程可能會(huì)進(jìn)入阻塞的狀態(tài),即Blocked
- Blocked:在該狀態(tài)下,線程暫停運(yùn)行,解除阻塞后,線程會(huì)進(jìn)入Runnable狀態(tài),等待CPU再次分配時(shí)間片給它
- 結(jié)束:線程方法執(zhí)行完畢或者因?yàn)楫惓=K止返回
這就和人的一生,出生-學(xué)習(xí)(工作之前的準(zhǔn)備)-工作-休假
其中最復(fù)雜的是線程從Running進(jìn)入Blocked狀態(tài),通常有三種情況:
- 睡眠:線程主動(dòng)調(diào)用sleep()或join()方法后.
- 等待:線程中調(diào)用wait()方法,此時(shí)需要有其他線程通過notify()方法來喚醒
- 同步:線程中獲取線程鎖,但是因?yàn)橘Y源已經(jīng)被其他線程占用時(shí).
到現(xiàn)在,我們對(duì)線程有個(gè)基本的概念,光說不練假把式,下面我們就通過是三個(gè)小的示例來聊聊線程的使用以及線程中最終的兩個(gè)概念:同步和通信.
線程簡(jiǎn)單使用
Python當(dāng)中要實(shí)現(xiàn)多線程有兩種方式:一種是使用低級(jí)的_thread模塊,另一種高級(jí)threading模塊,相比而言,我推薦使用threading模塊..在開始之前呢,先來了解下threading模塊給我提供哪些常用的類:
Thread,Lock,RLock,Condition,Event,Semaphore,Timer和Local.
這幾個(gè)類可謂開發(fā)多線程中的神兵利器.但是介于篇幅,咱就不展開講了.
我們直接來看如何使用多線程,這才是至關(guān)重要的,有句老話是這么說的:要想讓小孩子跑得先讓他學(xué)會(huì)走.我們這就走兩步:
import threading
#具體做啥事,寫在函數(shù)中
def run(number):
print(threading.currentThread().getName() + '\n')
print(number)
if __name__ == '__main__':
for i in range(10):
#注意這,開始咯,指明具體的方法和方法需要的參數(shù)
my_thread = threading.Thread(target=run, args=(i,))
#一定不要忘記
my_thread.start()
多線程的創(chuàng)建和運(yùn)行都是套路啊,寫的多了自然熟了,來看看運(yùn)行結(jié)果:
Thread-1,value=0
Thread-2,value=1
Thread-3,value=2
Thread-4,value=3
Thread-5,value=4
Thread-6,value=5
Thread-7,value=6
Thread-8,value=7
Thread-9,value=8
Thread-10,value=9
同步與通信
多線程開發(fā)中最難的問題不是如何使用,而是如何寫出正確高效的代碼,要寫出正確而高效的代碼必須要理解兩個(gè)很重要的概念:同步和通信.
所謂的通信指的是線程之間如何交換消息,而同步則用于控制不同線程之間操作發(fā)生的相對(duì)順序.簡(jiǎn)單點(diǎn)說同步就是控制多個(gè)線程訪問代碼的順序,通信就是線程之間如何傳遞消息.在python中實(shí)現(xiàn)同步的最簡(jiǎn)單的方案就是使用鎖機(jī)制,實(shí)現(xiàn)通信最簡(jiǎn)單的方案就是Event.下面就來看看這兩者的具體使用.
線程同步
當(dāng)多個(gè)線程同時(shí)訪問同一資源的時(shí)候,就會(huì)發(fā)生競(jìng)爭(zhēng),這有點(diǎn)像很多個(gè)男性都在追同一個(gè)妹紙一樣,結(jié)果是不可預(yù)期的.因此有必要使用某種機(jī)制來保證每個(gè)男生都有機(jī)會(huì)和女生相處,這有點(diǎn)像將小姑娘放在一間房子里,然后進(jìn)去的男生鎖上門,下一個(gè)男生要想進(jìn)去,必須等待上一個(gè)男生出來.只不過在這里叫線程鎖.
Python的threading模塊為我們提供了線程鎖功能,在threading中提供RLock對(duì)象,RLock對(duì)象內(nèi)部維護(hù)著一個(gè)Lock對(duì)象,它是一種可重入鎖。對(duì)于Lock對(duì)象而言荠锭,如果一個(gè)線程連續(xù)兩次進(jìn)行acquire操作,那么由于第一次acquire之后沒有release,第二次acquire將掛起線程精置。這會(huì)導(dǎo)致Lock對(duì)象永遠(yuǎn)不會(huì)release统翩,使得線程死鎖。而RLock對(duì)象允許一個(gè)線程多次對(duì)其進(jìn)行acquire操作耀找,因?yàn)樵谄鋬?nèi)部通過一個(gè)counter變量維護(hù)著線程acquire的次數(shù)翔悠。而且每一次的acquire操作必須有一個(gè)release操作與之對(duì)應(yīng),在所有的release操作完成之后野芒,別的線程才能申請(qǐng)?jiān)揜Lock對(duì)象.
通過鎖機(jī)制,最終多線程訪問共享資源的過程就類似以下:
上圖其實(shí)演示了在使用鎖來解決線程同步最本質(zhì)的一點(diǎn):將所有線程對(duì)共享資源的讀寫操作串行化.
同樣舉個(gè)簡(jiǎn)單的例子來演示RLock最簡(jiǎn)單的用法:
import threading
mylock = threading.RLock()
num = 0
class WorkThread(threading.Thread):
def __init__(self, name):
threading.Thread.__init__(self)
self.t_name = name
def run(self):
global num
while True:
mylock.acquire()
print('\n%s locked, number: %d' % (self.t_name, num))
if num >= 4:
mylock.release()
print('\n%s released, number: %d' % (self.t_name, num))
break
num += 1
print('\n%s released, number: %d' % (self.t_name, num))
mylock.release()
def test():
thread1 = WorkThread('A-Worker')
thread2 = WorkThread('B-Worker')
thread1.start()
thread2.start()
if __name__ == '__main__':
test()
來看看運(yùn)行結(jié)果:
A-Worker locked, number: 0
A-Worker released, number: 1
A-Worker locked, number: 1
A-Worker released, number: 2
A-Worker locked, number: 2
A-Worker released, number: 3
A-Worker locked, number: 3
A-Worker released, number: 4
A-Worker locked, number: 4
A-Worker released, number: 4
B-Worker locked, number: 4
B-Worker released, number: 4
有些同學(xué)會(huì)問除了Lock和RLock還有其他的方式來實(shí)現(xiàn)類似的效果么?當(dāng)然,比如Condition和Semaphore都有類似的功能,其中Condition是在Lock/RLock的基礎(chǔ)上再次包裝而成,而Semaphore的原理和操作系統(tǒng)的PV操作一致.之所以不細(xì)說的原因在于基本他們的基本使用和原理并無本質(zhì)區(qū)別.我個(gè)人也一直認(rèn)為越復(fù)雜的東西背后越是有簡(jiǎn)單的原理,當(dāng)然歡迎有興趣的同學(xué)和我進(jìn)行探討.
線程通信
在很多時(shí)候,我們需要在線程間傳遞消息,也叫作線程通信.Python中提供的Event就是最簡(jiǎn)單的通信機(jī)制之一.使用threading.Event可以使一個(gè)線程等待其他線程的通知蓄愁,我們把這個(gè)Event傳遞到線程對(duì)象中,Event默認(rèn)內(nèi)置了一個(gè)標(biāo)志狞悲,初始值為False撮抓。一旦該線程通過wait()方法進(jìn)入等待狀態(tài),直到另一個(gè)線程調(diào)用該Event的set()方法將內(nèi)置標(biāo)志設(shè)置為True時(shí)摇锋,該Event會(huì)通知所有等待狀態(tài)的線程恢復(fù)運(yùn)行丹拯。先來看看Event中一些常用的方法:
方法名 | 含義 |
---|---|
isSet() | 測(cè)試內(nèi)置的標(biāo)識(shí)是否為True |
set() | 將標(biāo)識(shí)設(shè)置為True,并通知所有處于阻塞狀態(tài)的線程恢復(fù)運(yùn)行 |
clear() | 將標(biāo)識(shí)設(shè)置為False |
wait([timeout]) | 如果標(biāo)識(shí)為True時(shí)立即返回,否則阻塞線程至阻塞狀態(tài),等待其他線程調(diào)用set() |
來看個(gè)簡(jiǎn)單示例,我們暫且假設(shè)你有6個(gè)妹紙需要叫她們起床,這時(shí)候你該怎么做呢?
import threading
import time
class WorkThread(threading.Thread):
def __init__(self, signal):
threading.Thread.__init__(self)
self.singal = signal
def run(self):
print("妹紙 %s,睡覺了 ..." % self.name)
self.singal.wait()
print("妹紙 %s, 起床..." % self.name)
if __name__ == "__main__":
singal = threading.Event()
for t in range(0, 6):
thread = WorkThread(singal)
thread.start()
print("三秒鐘后叫妹紙起床 ")
time.sleep(3)
#喚醒阻塞中的妹紙
singal.set()
這里的的你就充當(dāng)了主線程,每個(gè)妹紙就是一個(gè)子線程,不出意外三秒之后你就會(huì)按時(shí)喚醒所有的妹紙了:
妹紙 Thread-1,睡覺了 ...
妹紙 Thread-2,睡覺了 ...
妹紙 Thread-3,睡覺了 ...
妹紙 Thread-4,睡覺了 ...
妹紙 Thread-5,睡覺了 ...
妹紙 Thread-6,睡覺了 ...
三秒鐘后叫妹紙起床
妹紙 Thread-1, 起床...
妹紙 Thread-2, 起床...
妹紙 Thread-5, 起床...
妹紙 Thread-4, 起床...
妹紙 Thread-3, 起床...
妹紙 Thread-6, 起床...
使用Event實(shí)現(xiàn)線程通信通信固然可以,但是另一種進(jìn)行線程通信的方式是借助隊(duì)列,也就是Queue.在python的標(biāo)準(zhǔn)庫中提供了線程安全的隊(duì)列,基于FIFO(先進(jìn)先出)實(shí)現(xiàn),可以方便的幫助我們實(shí)現(xiàn)線程間的消息傳遞,使用非常簡(jiǎn)單,其原理也不難,用一張簡(jiǎn)單的圖展示:
另外,凡是符合該種結(jié)構(gòu)的多線程通信過程我們稱之為生產(chǎn)者-消費(fèi)者模型.
線程池
其實(shí),有關(guān)多線程的使用時(shí)非常簡(jiǎn)單的,更多的是根據(jù)具體的業(yè)務(wù)情況編寫相應(yīng)的邏輯.初次之外,考慮處理器的資源畢竟是有限的,不能一味的創(chuàng)建線程,我曾看到有些小伙伴在寫爬蟲的時(shí)候,100個(gè)url就創(chuàng)建了100個(gè)線程,其后果可想而知,因此當(dāng)有需求要用到很多線程時(shí),考慮使用線程池技術(shù).
另外我只推薦用于多線程用于處理有關(guān)I/O的操作,不然反而造成性能下降