淺談Python多線程

作者簡(jiǎn)介:

姓名:黃志成(小黃)

博客: 博客

線程

一.什么是線程?

操作系統(tǒng)原理相關(guān)的書僧鲁,基本都會(huì)提到一句很經(jīng)典的話: "進(jìn)程是資源分配的最小單位,線程則是CPU調(diào)度的最小單位"象泵。

線程是操作系統(tǒng)能夠進(jìn)行運(yùn)算調(diào)度的最小單位寞秃。它被包含在進(jìn)程之中,是進(jìn)程中的實(shí)際運(yùn)作單位偶惠。一條線程指的是進(jìn)程中一個(gè)單一順序的控制流春寿,一個(gè)進(jìn)程中可以并發(fā)多個(gè)線程,每條線程并行執(zhí)行不同的任務(wù)

好處 :

1.易于調(diào)度忽孽。

2.提高并發(fā)性绑改。通過線程可方便有效地實(shí)現(xiàn)并發(fā)性。進(jìn)程可創(chuàng)建多個(gè)線程來執(zhí)行同一程序的不同部分兄一。

3.開銷少厘线。創(chuàng)建線程比創(chuàng)建進(jìn)程要快,所需開銷很少出革。

4.利于充分發(fā)揮多處理器的功能造壮。通過創(chuàng)建多線程進(jìn)程,每個(gè)線程在一個(gè)處理器上運(yùn)行骂束,從而實(shí)現(xiàn)應(yīng)用程序的并發(fā)性耳璧,使每個(gè)處理器都得到充分運(yùn)行。

在解釋python多線程的時(shí)候. 先和大家分享一下 python 的GIL 機(jī)制展箱。

二.GIL(Global Interpreter Lock)全局解釋器鎖

Python代碼的執(zhí)行由Python 虛擬機(jī)(也叫解釋器主循環(huán)旨枯,CPython版本)來控制,Python 在設(shè)計(jì)之初就考慮到要在解釋器的主循環(huán)中混驰,同時(shí)只有一個(gè)線程在執(zhí)行攀隔,即在任意時(shí)刻,只有一個(gè)線程在解釋器中運(yùn)行栖榨。對(duì)Python 虛擬機(jī)的訪問由全局解釋器鎖(GIL)來控制竞慢,正是這個(gè)鎖能保證同一時(shí)刻只有一個(gè)線程在運(yùn)行。

在多線程環(huán)境中治泥,Python 虛擬機(jī)按以下方式執(zhí)行:

  1. 設(shè)置GIL
  2. 切換到一個(gè)線程去運(yùn)行
  3. 運(yùn)行:
    a. 指定數(shù)量的字節(jié)碼指令筹煮,或者
    b. 線程主動(dòng)讓出控制(可以調(diào)用time.sleep(0))
  4. 把線程設(shè)置為睡眠狀態(tài)
  5. 解鎖GIL
  6. 再次重復(fù)以上所有步驟

首先需要明確的一點(diǎn)是GIL并不是Python的特性,它是在實(shí)現(xiàn)Python解析器(CPython)時(shí)所引入的一個(gè)概念居夹。Python同樣一段代碼可以通過CPython败潦,PyPy本冲,Psyco等不同的Python執(zhí)行環(huán)境來執(zhí)行。像其中的JPython就沒有GIL劫扒。然而因?yàn)镃Python是大部分環(huán)境下默認(rèn)的Python執(zhí)行環(huán)境檬洞。所以在很多人的概念里CPython就是Python,也就想當(dāng)然的把GIL歸結(jié)為Python語言的缺陷沟饥。所以這里要先明確一點(diǎn):GIL并不是Python的特性添怔,Python完全可以不依賴于GIL

還有,就是在做I/O操作時(shí)贤旷,GIL總是會(huì)被釋放广料。對(duì)所有面向I/O 的(會(huì)調(diào)用內(nèi)建的操作系統(tǒng)C 代碼的)程序來說,GIL 會(huì)在這個(gè)I/O 調(diào)用之前被釋放幼驶,以允許其它的線程在這個(gè)線程等待I/O 的時(shí)候運(yùn)行艾杏。如果是純計(jì)算的程序,沒有 I/O 操作盅藻,解釋器會(huì)每隔 100 次操作就釋放這把鎖购桑,讓別的線程有機(jī)會(huì)執(zhí)行(這個(gè)次數(shù)可以通過 sys.setcheckinterval 來調(diào)整)如果某線程并未使用很多I/O 操作,它會(huì)在自己的時(shí)間片內(nèi)一直占用處理器(和GIL)氏淑。也就是說勃蜘,I/O 密集型的Python 程序比計(jì)算密集型的程序更能充分利用多線程環(huán)境的好處。

三.線程的生命周期

image

各個(gè)狀態(tài)說明:

  • New新建 :新創(chuàng)建的線程經(jīng)過初始化后假残,進(jìn)入Runnable狀態(tài)元旬。
  • Runnable就緒:等待線程調(diào)度。調(diào)度后進(jìn)入運(yùn)行狀態(tài)守问。
  • Running運(yùn)行:線程正常運(yùn)行
  • Blocked阻塞:暫停運(yùn)行匀归,解除阻塞后進(jìn)入Runnable狀態(tài)重新等待調(diào)度。
  • Dead消亡:線程方法執(zhí)行完畢返回或者異常終止耗帕。

可能有3種情況從Running進(jìn)入Blocked:

  • 同步:線程中獲取同步鎖穆端,但是資源已經(jīng)被其他線程鎖定時(shí),進(jìn)入Locked狀態(tài)仿便,直到該資源可獲忍鍐(獲取的順序由Lock隊(duì)列控制)
  • 睡眠:線程運(yùn)行sleep()或join()方法后,線程進(jìn)入Sleeping狀態(tài)嗽仪。區(qū)別在于sleep等待固定的時(shí)間荒勇,而join是等待子線程執(zhí)行完。sleep()確保先運(yùn)行其他線程中的方法闻坚。當(dāng)然join也可以指定一個(gè)“超時(shí)時(shí)間”沽翔。從語義上來說,如果兩個(gè)線程a,b, 在a中調(diào)用b.join(),相當(dāng)于合并(join)成一個(gè)線程仅偎。將會(huì)使主調(diào)線程(即a)堵塞(暫停運(yùn)行, 不占用CPU資源), 直到被調(diào)用線程運(yùn)行結(jié)束或超時(shí), 參數(shù)timeout是一個(gè)數(shù)值類型跨蟹,表示超時(shí)時(shí)間,如果未提供該參數(shù)橘沥,那么主調(diào)線程將一直堵塞到被調(diào)線程結(jié)束窗轩。最常見的情況是在主線程中join所有的子線程。
  • 等待:線程中執(zhí)行wait()方法后座咆,線程進(jìn)入Waiting狀態(tài)痢艺,等待其他線程的通知(notify)。wait方法釋放內(nèi)部所占用的瑣介陶,同時(shí)線程被掛起堤舒,直至接收到通知被喚醒或超時(shí)(如果提供了timeout參數(shù)的話)。當(dāng)線程被喚醒并重新占有瑣的時(shí)候斤蔓,程序才會(huì)繼續(xù)執(zhí)行下去植酥。

threading.Lock()不允許同一線程多次acquire(), 而RLock允許, 即多次出現(xiàn)acquire和release

四.Python threading模塊

上面介紹了這么多理論.下面我們用python提供的threading模塊來實(shí)現(xiàn)一個(gè)多線程的程序

threading 提供了兩種調(diào)用方式:

  • 直接調(diào)用
import threading

def func(n): # 定義每個(gè)線程要運(yùn)行的函數(shù)
    while n > 0:
        print("當(dāng)前線程數(shù):", threading.activeCount())
        n -= 1
        
for x in range(5):
    t = threading.Thread(target=func, args=(2,))  # 生成一個(gè)線程實(shí)例,生成實(shí)例后 并不會(huì)啟動(dòng),需要使用start命令
    t.start() #啟動(dòng)線程
  • 繼承式調(diào)用
class MyThread(threading.Thread): # 繼承threading的Thread類
    def __init__(self, num):
        threading.Thread.__init__(self) # 必須執(zhí)行父類的構(gòu)造方法
        self.num = num # 傳入?yún)?shù) num

    def run(self):  # 定義每個(gè)線程要運(yùn)行的函數(shù)
        while self.num > 0:
            print("當(dāng)前線程數(shù):", threading.activeCount())
            self.num -= 1

for x in range(5):
    t = MyThread(2) # 生成實(shí)例,傳入?yún)?shù)
    t.start() #啟動(dòng)線程

兩種方式都可以調(diào)用我們的多線程方法镀岛。

五.子線程阻塞

運(yùn)行下面的代碼,看看結(jié)果.

import threading
def func(n):
    while n > 0:
        print("當(dāng)前線程數(shù):", threading.activeCount())
        n -= 1
for x in range(5):
    t = threading.Thread(target=func, args=(2,))
    t.start()

print("主線程:", threading.current_thread().name)

運(yùn)行結(jié)果:

當(dāng)前線程數(shù): 2
當(dāng)前線程數(shù): 2
當(dāng)前線程數(shù): 2
當(dāng)前線程數(shù): 2
當(dāng)前線程數(shù): 2
當(dāng)前線程數(shù): 3
當(dāng)前線程數(shù): 3
當(dāng)前線程數(shù): 3
主線程: MainThread
當(dāng)前線程數(shù): 3
當(dāng)前線程數(shù): 3

那我們?nèi)绾巫枞泳€程讓他們運(yùn)行完,在繼續(xù)后面的操作呢.這個(gè)時(shí)候join()方法就派上用途了. 我們改寫代碼:

import threading

def func(n):
    while n > 0:
        print("當(dāng)前線程數(shù):", threading.activeCount())
        n -= 1

threads = [] #運(yùn)行的線程列表
for x in range(5):
    t = threading.Thread(target=func, args=(2,))
    threads.append(t) # 將子線程追加到列表
    t.start()

for t in threads:
    t.join()

print("主線程:", threading.current_thread().name)

join的原理就是依次檢驗(yàn)線程池中的線程是否結(jié)束弦牡,沒有結(jié)束就阻塞直到線程結(jié)束,如果結(jié)束則跳轉(zhuǎn)執(zhí)行下一個(gè)線程的join函數(shù)漂羊。

先看看這個(gè):

  1. 阻塞主進(jìn)程驾锰,專注于執(zhí)行多線程中的程序。

  2. 多線程多join的情況下走越,依次執(zhí)行各線程的join方法椭豫,前頭一個(gè)結(jié)束了才能執(zhí)行后面一個(gè)。

  3. 無參數(shù)旨指,則等待到該線程結(jié)束赏酥,才開始執(zhí)行下一個(gè)線程的join。

  4. 參數(shù)timeout為線程的阻塞時(shí)間谆构,如 timeout=2 就是罩著這個(gè)線程2s 以后裸扶,就不管他了,繼續(xù)執(zhí)行下面的代碼搬素。

六.線程鎖(互斥鎖)

一個(gè)進(jìn)程可以開啟多個(gè)線程,那么多么多個(gè)進(jìn)程操作相同數(shù)據(jù),勢(shì)必會(huì)出現(xiàn)沖突.那如何避免這種問題呢?

import threading,time

num = 10 #共享變量

def func():
    global num
    lock.acquire() # 加鎖
    num = num - 1
    lock.release() # 解鎖
    print(num)

threads = []
lock = threading.Lock() #生成全局鎖
for x in range(10):
    t = threading.Thread(target=func)
    threads.append(t)
    t.start()

for t in threads:
    t.join()

通過 threading.Lock() 我們可以申請(qǐng)一個(gè)鎖呵晨。然后 acquire 方法進(jìn)入臨界區(qū).操作完共享數(shù)據(jù) 使用 release 方法退出.

臨界區(qū)的概念: 百度百科

在這里補(bǔ)充一下:Python的Queue模塊是線程安全的.可以不對(duì)它加鎖操作.

聰明的同學(xué) 會(huì)發(fā)現(xiàn)一個(gè)問題? 咱們不是有 GIL 嗎 為什么還要加鎖?

這個(gè)問題問的好熬尺!我們下一節(jié),將對(duì)這個(gè)問題進(jìn)行探討.

七.LOCK 和 GIL

GIL的鎖是對(duì)于一個(gè)解釋器摸屠,只能有一個(gè)thread在執(zhí)行bytecode。所以每時(shí)每刻只有一條bytecode在被執(zhí)行一個(gè)thread粱哼。GIL保證了bytecode 這層面上是線程是安全的.

但是如果你有個(gè)操作一個(gè)共享 x += 1季二,這個(gè)操作需要多個(gè)bytecodes操作,在執(zhí)行這個(gè)操作的多條bytecodes期間的時(shí)候可能中途就換thread了揭措,這樣就出現(xiàn)了線程不安全的情況了戒傻。

總結(jié):同一時(shí)刻CPU上只有單個(gè)執(zhí)行流不代表線程安全税手。

八.信號(hào)量

互斥鎖 同時(shí)只允許一個(gè)線程更改數(shù)據(jù),而Semaphore是同時(shí)允許一定數(shù)量的線程更改數(shù)據(jù) 需纳,比如廁所有3個(gè)坑芦倒,那最多只允許3個(gè)人上廁所,后面的人只能等里面有人出來了才能再進(jìn)去不翩。

import threading,time

num = 10

def func():
    global num
    lock.acquire()
    time.sleep(2)
    num = num - 1
    lock.release()
    print(num)

threads = []
lock = threading.BoundedSemaphore(5) #最多允許5個(gè)線程同時(shí)運(yùn)行
for x in range(10):
    t = threading.Thread(target=func)
    threads.append(t)
    t.start()

for t in threads:
    t.join()

print("主線程:", threading.current_thread().name)

運(yùn)行一下上面的代碼.你會(huì)很明顯的發(fā)現(xiàn) 每次只執(zhí)行五個(gè)線程兵扬。

參考文獻(xiàn)

淺談多進(jìn)程多線程的選擇: 文章鏈接

python-多線程(原理篇): 文章鏈接

Python有GIL為什么還需要線程同步?: 文章鏈接

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末口蝠,一起剝皮案震驚了整個(gè)濱河市器钟,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌妙蔗,老刑警劉巖傲霸,帶你破解...
    沈念sama閱讀 216,372評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異眉反,居然都是意外死亡昙啄,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門寸五,熙熙樓的掌柜王于貴愁眉苦臉地迎上來梳凛,“玉大人,你說我怎么就攤上這事梳杏∪途埽” “怎么了?”我有些...
    開封第一講書人閱讀 162,415評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵十性,是天一觀的道長(zhǎng)叛溢。 經(jīng)常有香客問我,道長(zhǎng)劲适,這世上最難降的妖魔是什么楷掉? 我笑而不...
    開封第一講書人閱讀 58,157評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮减响,結(jié)果婚禮上靖诗,老公的妹妹穿的比我還像新娘。我一直安慰自己支示,他們只是感情好刊橘,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評(píng)論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著颂鸿,像睡著了一般促绵。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,125評(píng)論 1 297
  • 那天败晴,我揣著相機(jī)與錄音浓冒,去河邊找鬼。 笑死尖坤,一個(gè)胖子當(dāng)著我的面吹牛稳懒,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播慢味,決...
    沈念sama閱讀 40,028評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼场梆,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了纯路?” 一聲冷哼從身側(cè)響起或油,我...
    開封第一講書人閱讀 38,887評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎驰唬,沒想到半個(gè)月后顶岸,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,310評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡叫编,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評(píng)論 2 332
  • 正文 我和宋清朗相戀三年辖佣,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片宵溅。...
    茶點(diǎn)故事閱讀 39,690評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡凌简,死狀恐怖上炎,靈堂內(nèi)的尸體忽然破棺而出恃逻,到底是詐尸還是另有隱情,我是刑警寧澤藕施,帶...
    沈念sama閱讀 35,411評(píng)論 5 343
  • 正文 年R本政府宣布寇损,位于F島的核電站,受9級(jí)特大地震影響裳食,放射性物質(zhì)發(fā)生泄漏矛市。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評(píng)論 3 325
  • 文/蒙蒙 一诲祸、第九天 我趴在偏房一處隱蔽的房頂上張望浊吏。 院中可真熱鬧,春花似錦救氯、人聲如沸找田。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽墩衙。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間漆改,已是汗流浹背心铃。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評(píng)論 1 268
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留挫剑,地道東北人去扣。 一個(gè)月前我還...
    沈念sama閱讀 47,693評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像樊破,于是被迫代替她去往敵國(guó)和親厅篓。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評(píng)論 2 353

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

  • 一. 操作系統(tǒng)概念 操作系統(tǒng)位于底層硬件與應(yīng)用軟件之間的一層.工作方式: 向下管理硬件,向上提供接口.操作系統(tǒng)進(jìn)行...
    月亮是我踢彎得閱讀 5,965評(píng)論 3 28
  • 必備的理論基礎(chǔ) 1.操作系統(tǒng)作用: 隱藏丑陋復(fù)雜的硬件接口捶码,提供良好的抽象接口羽氮。 管理調(diào)度進(jìn)程,并將多個(gè)進(jìn)程對(duì)硬件...
    drfung閱讀 3,537評(píng)論 0 5
  • 多進(jìn)程 要讓python程序?qū)崿F(xiàn)多進(jìn)程惫恼,我們先了解操作系統(tǒng)的相關(guān)知識(shí)档押。 Unix、Linux操作系統(tǒng)提供了一個(gè)fo...
    蓓蓓的萬能男友閱讀 595評(píng)論 0 1
  • 閑翻雜志祈纯,目光被一張照片吸住了令宿。 綠色的藤蔓下面,垂掛著兩個(gè)細(xì)長(zhǎng)的苦瓜腕窥。青碧映目粒没,涼爽逼人,遍體的暑熱與心里的燥煩...
    鉛筆芒種閱讀 437評(píng)論 0 1
  • “任何人都可以作畫”簇爆,摩西奶奶這樣說過癞松。她還說“任何年齡的人都可以作畫∪肭”摩西奶奶是聞名全球的風(fēng)俗畫畫家响蓉。她從77...
    Tianjiejie閱讀 120評(píng)論 0 0