python之greenlet

python之gevent(1)一文中我們簡單的介紹了gevent的使用。python由于GIL的原因,導(dǎo)致線程性能嚴(yán)重下降,實(shí)際可以認(rèn)為是偽線程,無法達(dá)到我們?cè)谑褂镁€程時(shí)候的預(yù)期。而gevent就是一個(gè)現(xiàn)在很火、支持也很全面的python第三方協(xié)程庫,可以讓python代碼很方便的使用線程。在更深入的學(xué)習(xí)gevent的源碼前,我們先一起學(xué)習(xí)了解一下gevent實(shí)現(xiàn)的基礎(chǔ)——greenlet。

Greenlet是python的一個(gè)C擴(kuò)展,旨在提供可自行調(diào)度的‘微線程’, 即協(xié)程。generator實(shí)現(xiàn)的協(xié)程在yield value時(shí)只能將value返回給調(diào)用者(caller)。 而在greenlet中,target.switch(value)可以切換到指定的協(xié)程(target), 然后yield value。greenlet用switch來表示協(xié)程的切換,從一個(gè)協(xié)程切換到另一個(gè)協(xié)程需要顯式指定。

greenlet初探

一下是官網(wǎng)給出的第一個(gè)例子:

from greenlet import greenlet
def test1():
    print 12
    gr2.switch()
    print 34

def test2():
    print 56
    gr1.switch()
    print 78

gr1 = greenlet(test1)
gr2 = greenlet(test2)
gr1.switch()

運(yùn)行代碼,輸出為:12 56 34

當(dāng)創(chuàng)建一個(gè)greenlet時(shí),首先初始化一個(gè)空的棧, switch到這個(gè)棧的時(shí)候,會(huì)運(yùn)行在greenlet構(gòu)造時(shí)傳入的函數(shù)(首先在test1中打印 12), 如果在這個(gè)函數(shù)(test1)中switch到其他協(xié)程(到了test2 打印34),那么該協(xié)程會(huì)被掛起,等到切換回來(在test2中切換回來 打印34)。當(dāng)這個(gè)協(xié)程對(duì)應(yīng)函數(shù)執(zhí)行完畢,那么這個(gè)協(xié)程就變成dead狀態(tài)。

greenlet的module與class

一起看一下greenlet中的屬性:


image.png

其中,比較重要的是getcurrent(), 類greenlet、異常類GreenletExit。
getcurrent()返回當(dāng)前的greenlet實(shí)例;
GreenletExit:是一個(gè)特殊的異常,當(dāng)觸發(fā)了這個(gè)異常的時(shí)候,即使不處理,也不會(huì)拋到其parent(后面會(huì)提到協(xié)程中對(duì)返回值或者異常的處理)

然后我們?cè)賮砜纯磄reenlet.greenlet這個(gè)類:


image.png

比較重要的幾個(gè)屬性:
  run:當(dāng)greenlet啟動(dòng)的時(shí)候會(huì)調(diào)用到這個(gè)callable,如果我們需要繼承g(shù)reenlet.greenlet時(shí),需要重寫該方法
  switch:前面已經(jīng)介紹過了,在greenlet之間切換
  parent:可讀寫屬性,后面介紹
  dead:如果greenlet執(zhí)行結(jié)束,那么該屬性為true
  throw:切換到指定greenlet后立即跑出異常

注意,本文后面提到的greenlet大多都是指greenlet.greenlet這個(gè)class,注意區(qū)分

Switch not call

對(duì)于greenlet,最常用的寫法是 x = gr.switch(y)。 這句話的意思是切換到gr,傳入?yún)?shù)y。當(dāng)從其他協(xié)程(不一定是這個(gè)gr)切換回來的時(shí)候,將值付給x。

import greenlet
def test1(x, y):
    z = gr2.switch(x+y)
    print 'test1 ', z

def test2(u):
    print 'test2 ', u
    gr1.switch(10)

gr1 = greenlet.greenlet(test1)
gr2 = greenlet.greenlet(test2)
print gr1.switch("hello", " world")

輸出:
    'test2 ' 'hello world'
    'test1 ' 10
    None
上面的例子,第12行從main greenlet切換到了gr1,test1第3行切換到了gs2,然后gr1掛起,第8行從gr2切回gr1時(shí),將值(10)返回值給了 z。

每一個(gè)Greenlet都有一個(gè)parent,一個(gè)新的greenlet在哪里創(chuàng)生,當(dāng)前環(huán)境的greenlet就是這個(gè)新greenlet的parent。所有的greenlet構(gòu)成一棵樹,其跟節(jié)點(diǎn)就是還沒有手動(dòng)創(chuàng)建greenlet時(shí)候的”main” greenlet(事實(shí)上,在首次import greenlet的時(shí)候?qū)嵗?。?dāng)一個(gè)協(xié)程 正常結(jié)束,執(zhí)行流程回到其對(duì)應(yīng)的parent;或者在一個(gè)協(xié)程中拋出未被捕獲的異常,該異常也是傳遞到其parent。學(xué)習(xí)python的時(shí)候,有一句話會(huì)被無數(shù)次重復(fù)”everything is oblect”, 在學(xué)習(xí)greenlet的調(diào)用中,同樣有一句話應(yīng)該深刻理解, “switch not call”。

import greenlet
def test1(x, y):
    print id(greenlet.getcurrent()), id(greenlet.getcurrent().parent) # 40240272 40239952
    z = gr2.switch(x+y)
    print 'back z', z

def test2(u):
    print id(greenlet.getcurrent()), id(greenlet.getcurrent().parent) # 40240352 40239952
    return 'hehe'

gr1 = greenlet.greenlet(test1)
gr2 = greenlet.greenlet(test2)
print id(greenlet.getcurrent()), id(gr1), id(gr2)     # 40239952, 40240272, 40240352
print gr1.switch("hello", " world"), 'back to main'    # hehe back to main

由這個(gè)例子可以看出,盡管是從test1所在的協(xié)程gr1 切換到了gr2,但gr2的parent還是’main’ greenlet,因?yàn)槟J(rèn)的parent取決于greenlet的創(chuàng)生環(huán)境。另外,在test2中return之后整個(gè)返回值返回到了其parent,而不是switch到該協(xié)程的地方(即不是test1),這個(gè)跟我們平時(shí)的函數(shù)調(diào)用不一樣,記住“switch not call”。對(duì)于異常,也是展開至parent:

import greenlet
def test1(x, y):
    try:
        z = gr2.switch(x+y)
    except Exception:
        print 'catch Exception in test1'

def test2(u):
    assert False

gr1 = greenlet.greenlet(test1)
gr2 = greenlet.greenlet(test2)
try:
    gr1.switch("hello", " world")
except:
    print 'catch Exception in main'

輸出為:
   catch Exception in main

greenlet生命周期

本文開始的地方提到第一個(gè)例子中的gr2其實(shí)并沒有正常結(jié)束,我們可以用greenlet.dead這個(gè)屬性來查看:

from greenlet import greenlet
def test1():
    gr2.switch(1)
    print 'test1 finished'

def test2(x):
    print 'test2 first', x
    z = gr1.switch()
    print 'test2 back', z

gr1 = greenlet(test1)
gr2 = greenlet(test2)
gr1.switch()
print 'gr1 is dead?: %s, gr2 is dead?: %s' % (gr1.dead, gr2.dead)
gr2.switch()
print 'gr1 is dead?: %s, gr2 is dead?: %s' % (gr1.dead, gr2.dead)
print gr2.switch(10)

輸出如下:


image.png

從這個(gè)例子可以看出:
1.只有當(dāng)協(xié)程對(duì)應(yīng)的函數(shù)執(zhí)行完畢,協(xié)程才會(huì)die,所以第一次Check的時(shí)候gr2并沒有die,因?yàn)榈?行切換出去了就沒切回來。在main中再switch到gr2的時(shí)候, 執(zhí)行后面的邏輯,gr2 die
2.如果試圖再次switch到一個(gè)已經(jīng)是dead狀態(tài)的greenlet會(huì)怎么樣呢,事實(shí)上會(huì)切換到其parent greenlet。

Greenlet Traceing

Greenlet也提供了接口使得程序員可以監(jiān)控greenlet的整個(gè)調(diào)度流程。主要是gettrace 和 settrace(callback)函數(shù)。

def test_greenlet_tracing():
    def callback(event, args):
        print event, 'from', id(args[0]), 'to', id(args[1])

    def dummy():
        g2.switch()

    def dummyexception():
        raise Exception('excep in coroutine')

    main = greenlet.getcurrent()
    g1 = greenlet.greenlet(dummy)
    g2 = greenlet.greenlet(dummyexception)
    print 'main id %s, gr1 id %s, gr2 id %s' % (id(main), id(g1), id(g2))
    oldtrace = greenlet.settrace(callback)
    try:
        g1.switch()
    except:
        print 'Exception'
    finally:
        greenlet.settrace(oldtrace)

test_greenlet_tracing()  
image.png

其中callback函數(shù)event是switch或者throw之一,表明是正常調(diào)度還是異常跑出;args是二元組,表示是從協(xié)程args[0]切換到了協(xié)程args[1]。上面的輸出展示了切換流程:從main到gr1,然后到gr2,最后回到main。

greenlet使用建議

使用greenlet需要注意一下三點(diǎn):
  第一:greenlet創(chuàng)生之后,一定要結(jié)束,不能switch出去就不回來了,否則容易造成內(nèi)存泄露
  第二:python中每個(gè)線程都有自己的main greenlet及其對(duì)應(yīng)的sub-greenlet ,不能線程之間的greenlet是不能相互切換的
  第三:不能存在循環(huán)引用,這個(gè)是官方文檔明確說明

”Greenlets do not participate in garbage collection; cycles involving data that is present in a greenlet’s frames will not be detected. “

來看一個(gè)例子:

from greenlet import greenlet, GreenletExit

huge = []

def show_leak():
    def test1():
        gr2.switch()

    def test2():
        huge.extend([x* x for x in range(100)])
        gr1.switch()
        print 'finish switch del huge'
        del huge[:]
    
    gr1 = greenlet(test1)
    gr2 = greenlet(test2)
    gr1.switch()
    gr1 = gr2 = None
    print 'length of huge is zero ? %s' % len(huge)

if __name__ == '__main__':
    show_leak() 
   # output: length of huge is zero ? 100

在test2函數(shù)中,第11行,我們將huge清空,然后再第16行將gr1、gr2的引用計(jì)數(shù)降到了0。但運(yùn)行結(jié)果告訴我們,第11行并沒有執(zhí)行,所以如果一個(gè)協(xié)程沒有正常結(jié)束是很危險(xiǎn)的,往往不符合程序員的預(yù)期。greenlet提供了解決這個(gè)問題的辦法,官網(wǎng)文檔提到:如果一個(gè)greenlet實(shí)例的引用計(jì)數(shù)變成0,那么會(huì)在上次掛起的地方拋出GreenletExit異常,這就使得我們可以通過try ... finally 處理資源泄露的情況。如下面的代碼:

from greenlet import greenlet, GreenletExit

huge = []

def show_leak():
    def test1():
        gr2.switch()

    def test2():
        huge.extend([x* x for x in range(100)])
        try:
            gr1.switch()
        finally:
            print 'finish switch del huge'
            del huge[:]
    
    gr1 = greenlet(test1)
    gr2 = greenlet(test2)
    gr1.switch()
    gr1 = gr2 = None
    print 'length of huge is zero ? %s' % len(huge)

if __name__ == '__main__':
    show_leak()
    # output :
    # finish switch del huge
   # length of huge is zero ? 0

上述代碼的switch流程:main greenlet --> gr1 --> gr2 --> gr1 --> main greenlet, 很明顯gr2沒有正常結(jié)束(在第10行掛起了)。第18行之后gr1,gr2的引用計(jì)數(shù)都變成0,那么會(huì)在第10行拋出GreenletExit異常,因此finally語句有機(jī)會(huì)執(zhí)行。同時(shí),在文章開始介紹Greenlet module的時(shí)候也提到了,GreenletExit這個(gè)異常并不會(huì)拋出到parent,所以main greenlet也不會(huì)出異常。

看上去貌似解決了問題,但這對(duì)程序員要求太高了,百密一疏。所以最好的辦法還是保證協(xié)程的正常結(jié)束。

以上便是greenlet的基本使用,下一次我們將在此基礎(chǔ)上繼續(xù)進(jìn)行g(shù)event的學(xué)習(xí)。

喜歡的朋友歡迎點(diǎn)個(gè)贊再走哈哈。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容