Python多線程入門

一直懶得寫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)的含義:

  1. 新建:使用線程的第一步就是創(chuàng)建線程,創(chuàng)建后的線程只是進(jìn)入可執(zhí)行的狀態(tài),也就是Runnable
  2. Runnable:進(jìn)入此狀態(tài)的線程還并未開始運(yùn)行,一旦CPU分配時(shí)間片給這個(gè)線程后,該線程才正式的開始運(yùn)行
  3. Running:線程正式開始運(yùn)行,在運(yùn)行過程中線程可能會(huì)進(jìn)入阻塞的狀態(tài),即Blocked
  4. Blocked:在該狀態(tài)下,線程暫停運(yùn)行,解除阻塞后,線程會(huì)進(jìn)入Runnable狀態(tài),等待CPU再次分配時(shí)間片給它
  5. 結(jié)束:線程方法執(zhí)行完畢或者因?yàn)楫惓=K止返回

這就和人的一生,出生-學(xué)習(xí)(工作之前的準(zhǔn)備)-工作-休假

其中最復(fù)雜的是線程從Running進(jìn)入Blocked狀態(tài),通常有三種情況:

  1. 睡眠:線程主動(dòng)調(diào)用sleep()或join()方法后.
  2. 等待:線程中調(diào)用wait()方法,此時(shí)需要有其他線程通過notify()方法來喚醒
  3. 同步:線程中獲取線程鎖,但是因?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的操作,不然反而造成性能下降

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市荸恕,隨后出現(xiàn)的幾起案子乖酬,更是在濱河造成了極大的恐慌,老刑警劉巖戚炫,帶你破解...
    沈念sama閱讀 212,816評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件剑刑,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡双肤,警方通過查閱死者的電腦和手機(jī)施掏,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,729評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來茅糜,“玉大人七芭,你說我怎么就攤上這事∶镒福” “怎么了狸驳?”我有些...
    開封第一講書人閱讀 158,300評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)缩赛。 經(jīng)常有香客問我耙箍,道長(zhǎng),這世上最難降的妖魔是什么酥馍? 我笑而不...
    開封第一講書人閱讀 56,780評(píng)論 1 285
  • 正文 為了忘掉前任辩昆,我火速辦了婚禮,結(jié)果婚禮上旨袒,老公的妹妹穿的比我還像新娘汁针。我一直安慰自己术辐,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,890評(píng)論 6 385
  • 文/花漫 我一把揭開白布施无。 她就那樣靜靜地躺著辉词,像睡著了一般。 火紅的嫁衣襯著肌膚如雪猾骡。 梳的紋絲不亂的頭發(fā)上瑞躺,一...
    開封第一講書人閱讀 50,084評(píng)論 1 291
  • 那天,我揣著相機(jī)與錄音卓练,去河邊找鬼隘蝎。 笑死,一個(gè)胖子當(dāng)著我的面吹牛襟企,可吹牛的內(nèi)容都是我干的嘱么。 我是一名探鬼主播,決...
    沈念sama閱讀 39,151評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼顽悼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼曼振!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起蔚龙,我...
    開封第一講書人閱讀 37,912評(píng)論 0 268
  • 序言:老撾萬榮一對(duì)情侶失蹤冰评,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后木羹,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體甲雅,經(jīng)...
    沈念sama閱讀 44,355評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,666評(píng)論 2 327
  • 正文 我和宋清朗相戀三年坑填,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了抛人。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,809評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡脐瑰,死狀恐怖妖枚,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情苍在,我是刑警寧澤绝页,帶...
    沈念sama閱讀 34,504評(píng)論 4 334
  • 正文 年R本政府宣布,位于F島的核電站寂恬,受9級(jí)特大地震影響续誉,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜初肉,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,150評(píng)論 3 317
  • 文/蒙蒙 一屈芜、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦井佑、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,882評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至盯拱,卻和暖如春盒发,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背狡逢。 一陣腳步聲響...
    開封第一講書人閱讀 32,121評(píng)論 1 267
  • 我被黑心中介騙來泰國打工宁舰, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人奢浑。 一個(gè)月前我還...
    沈念sama閱讀 46,628評(píng)論 2 362
  • 正文 我出身青樓蛮艰,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國和親雀彼。 傳聞我的和親對(duì)象是個(gè)殘疾皇子壤蚜,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,724評(píng)論 2 351

推薦閱讀更多精彩內(nèi)容

  • 線程狀態(tài)新建,就緒徊哑,運(yùn)行袜刷,阻塞,死亡莺丑。 線程同步多線程可以同時(shí)運(yùn)行多個(gè)任務(wù)著蟹,線程需要共享數(shù)據(jù)的時(shí)候,可能出現(xiàn)數(shù)據(jù)不...
    KevinCool閱讀 798評(píng)論 0 0
  • 進(jìn)程與線程的區(qū)別 現(xiàn)在梢莽,多核CPU已經(jīng)非常普及了萧豆,但是,即使過去的單核CPU蟹漓,也可以執(zhí)行多任務(wù)炕横。由于CPU執(zhí)行代碼...
    蘇糊閱讀 763評(píng)論 0 2
  • 所有代碼來自python核心編程 參考python核心編程一書,學(xué)習(xí)多線程工作模式葡粒,多線程實(shí)現(xiàn)主要模塊thread...
    Ssop閱讀 622評(píng)論 0 1
  • 目錄 一、開啟線程的兩種方式 在python中開啟線程要導(dǎo)入threading梅肤,它與開啟進(jìn)程所需要導(dǎo)入的模塊mul...
    CaiGuangyin閱讀 2,400評(píng)論 1 16
  • 下班的途中司蔬,特意進(jìn)了水果店買了香蕉和葡萄,再進(jìn)一家商店買了餅餅和能量棒姨蝴,為看歐洲杯準(zhǔn)備食糧俊啼。看球也是個(gè)重消耗的活左医,...
    昨夜風(fēng)鈴閱讀 243評(píng)論 0 0