greenlet是用C語言編寫的一個模塊,而後讓python調用,目的就是爲了讓python支持協程。python
A 「greenlet」 is a small independent pseudo-thread. Think about it as a small stack of frames; the outermost (bottom) frame is the initial function you called, and the innermost frame is the one in which the greenlet is currently paused. You work with greenlets by creating a number of such stacks and jumping execution between them. Jumps are never implicit: a greenlet must choose to jump to another greenlet, which will cause the former to suspend and the latter to resume where it was suspended. Jumping between greenlets is called 「switching」.網絡
When you create a greenlet, it gets an initially empty stack; when you first switch to it, it starts to run a specified function, which may call other functions, switch out of the greenlet, etc. When eventually the outermost function finishes its execution, the greenlet’s stack becomes empty again and the greenlet is 「dead」. Greenlets can also die of an uncaught exception.併發
一個"greenlet"是一個小型、獨立的僞線程。能夠把它想象成一個小型的棧幀,棧底是函數初始調用的位置,棧頂是當前greenlet暫停的位置。你可使用greenlet建立一堆這樣的堆棧,而後在它們之間切換執行。跳轉不能是隱式的,必需要顯式地指定要跳轉到另外一個greenlet,這就會使得前一個掛起,後一個從其以前掛起的地方恢復執行。不一樣greenlet之間的切換,咱們稱之爲"switching"框架
當你建立一個greenlet,它將獲得一個初始化爲空的棧。當你第一次切換到它,它會開始運行指定的函數,或許這個函數還會調用其餘的函數、切換跳出greenlet等等,當最終棧底函數執行完畢出棧的時候,這個greenlet的棧再次變爲空、"死掉"了。同時greenlet也會由於未捕捉的異常而死掉。socket
from greenlet import greenlet def foo(): print("foo start") gr2.switch() print("foo end") def bar(): print("bar start") gr1.switch() print("bar end") gr1 = greenlet(foo) gr2 = greenlet(bar) # 以上至關於建立了gr一、gr2兩個greenlet # 可是咱們說了,這至關於建立兩個初始化爲空的棧,只有當你切換到它,纔會運行指定的函數。 # 調用switch表示切換 gr1.switch() """ foo start bar start foo end """
經過上面代碼咱們來看一下流程:async
定義兩個函數,並建立兩個greenlet,此時棧爲空
一旦進行switch,便會執行相應的函數。好比這裏的gr1.switch(),便會執行foo函數
print以後,gr2.switch(),表示切換到另外一個greenlet。所以會先將當前函數執行的上下文、以及CPU寄存器的內容表示起來。
當執行bar函數,print以後咱們又切換到了gr1,那麼會經過上下文從暫停的地方恢復執行。
print以後這個函數就完全的執行完畢了,那麼會出棧,這個greenlet就死掉了,所以bar函數裏面的bar end是不會被打印的
所以咱們能夠認識到,greenlet並非一種真正意義上並行機制,它只是在同一個線程裏面,在不一樣的代碼塊之間進行切換,正如單核cpu在不一樣的進程之間切換同樣,這個執行一下子、那個執行一下子,只不過greenlet在切換的時候須要顯示指定切換到哪兒。編輯器
Let’s see where execution goes when a greenlet dies. Every greenlet has a 「parent」 greenlet. The parent greenlet is initially the one in which the greenlet was created (this can be changed at any time). The parent is where execution continues when a greenlet dies. This way, greenlets are organized in a tree. Top-level code that doesn’t run in a user-created greenlet runs in the implicit 「main」 greenlet, which is the root of the tree.函數
In the above example, both gr1 and gr2 have the main greenlet as a parent. Whenever one of them dies, the execution comes back to 「main」.oop
Uncaught exceptions are propagated into the parent, too. For example, if the above test2() contained a typo, it would generate a NameError that would kill gr2, and the exception would go back directly into 「main」. The traceback would show test2, but not test1. Remember, switches are not calls, but transfer of execution between parallel 「stack containers」, and the 「parent」 defines which stack logically comes 「below」 the current one.性能
接下來看看當一個greenlet死亡以後,執行點會去哪裏呢。每個greenlet都有一個父greenlet,而且都在其父greenlet中被建立(不過能夠在任意時刻改變)。當子greenlet結束時,執行位置從父greenlet那裏繼續,這樣,全部的greenlet之間就組合成了一顆樹,頂級的代碼不在用戶建立的greenlet中運行,而是運行在一個主greenlet中,即,全部greenlet的樹根
好比咱們以前的gr1和gr2都將主greenlet做爲父greenlet,任何一個死掉,執行點都會回到主greenlet
未捕獲的異常會傳遞給父greenlet,好比bar函數當中出現了異常,那麼這個異常將會直接傳遞給主greenlet。traceback將會顯示bar,而不是foo。記住:切換不是調用,而是執行點在並行的棧容器之間交換,而父greenlet定義了這些棧的前後關係。
注意的是,green是一個類,可不是一個函數。那麼它的屬性有哪些呢?咱們看一下源碼,固然這個源碼底層是C寫的,可是我用的是pycharm,這個編輯器把代碼抽象出來了。
class greenlet(object): def getcurrent(self, *args, **kwargs): # real signature unknown pass def gettrace(self, *args, **kwargs): # real signature unknown pass def settrace(self, *args, **kwargs): # real signature unknown pass def switch(self, *args, **kwargs): # real signature unknown; restored from __doc__ pass def throw(self, *args, **kwargs): # real signature unknown pass
這個咱們剛纔已經看到過了,就是切換到指定的greenlet執行,並且這裏面仍是能夠傳參的。
from greenlet import greenlet def foo(name, age): print(name, age) gr1 = greenlet(foo) gr1.switch("satori", age=16) """ satori 16 """
from greenlet import greenlet def foo(name): gr2.switch(16) print(f"name is {name}") def bar(age): print(f"age is {age}") gr1.switch() gr1 = greenlet(foo) gr2 = greenlet(bar) gr1.switch("satori") """ age is 16 name is satori """
首先gr1.switch("satori"),會調用foo,並把參數傳給name。而後調用gr2.switch(16),調用bar,並把參數傳遞給age而後打印,此時在bar中又調用了gr1.switch(),可是注意這裏沒有傳入參數。其實這裏傳不傳都無所謂,由於在初始化的時候函數棧是空的,在第一次gr1.switch的時候,已經傳了,棧幀已經建立了。 bar裏面的switch只不過是切換到了gr1這個greenlet當中,name仍是咱們第一次傳入的"satori"。所以greenlet的switch是在不一樣的greenlet之間進行切換,並非調用。
既然如此,若是我在bar裏面的switch裏面傳入值的話,那麼能不能在foo裏面接收到這個值呢?顯然是能夠的,怎麼接受,咱們能夠想象一下生成器裏面send
from greenlet import greenlet def foo(name): x = gr2.switch(16) print(x) print(f"name is {name}") def bar(age): print(f"age is {age}") gr1.switch("我向你傳了一個參數") gr1 = greenlet(foo) gr2 = greenlet(bar) gr1.switch("satori") """ age is 16 我向你傳了一個參數 name is satori """
能夠看到這個生成器很相似,yield xx是能夠賦值的,好比value = yield xx,那麼當我從這裏繼續前進的時候,能夠經過send("aaa"),那麼value就是咱們在send裏面傳入的aaa。同理switch也是同樣,咱們也能夠在bar的switch裏面進行傳參,這個參數就是foo裏面gr1.switch函數的返回值。若是熟悉生成器和小夥伴,能夠對比一下yield和send,會很容易理解。總結一下就是:函數的參數是咱們第一次switch的時候傳遞的,以後switch裏面傳遞值就會做爲別的switch的返回值。
咱們以前說每個greenlet都有一個parent greenlet,那麼這個父greenlet是誰呢?又是在哪裏建立的呢?事實上,在咱們import greenlet的時候,這個父greenlet就已經建立好了,全部的greenlet組成了一個樹,這個樹的根節點就是咱們尚未手動建立greenlet時候的main greenlet。當一個協程正常結束,執行流程會回到其對應的parent;或者一個協程中出現了未被捕獲的異常,該異常也是會回到其對應的parent。
from greenlet import greenlet def foo(): print("foo>>>", id(gr1.getcurrent()), id(gr1.getcurrent().parent)) return "xxx" def bar(): print("bar>>>", id(gr2.getcurrent()), id(gr2.getcurrent().parent)) gr1 = greenlet(foo) gr2 = greenlet(bar) gr1.switch() gr2.switch() """ foo>>> 2902341415936 2902341415776 bar>>> 2902341416096 2902341415776 """
能夠看到gr1和gr2不是同個對象,可是它們的父greenlet是同一個對象。由於默認的父greenlet取決於建立環境,同理foo函數的返回值也是返回給了父greenlet,你經過switch是獲取不到的。
from greenlet import greenlet def foo(): return "xxx" gr1 = greenlet(foo) print(gr1.switch()) # xxx # 這樣是能夠拿到返回值的
同理對於異常也是如此
from greenlet import greenlet def foo(): try: gr2.switch() except Exception: print("在foo中捕獲了bar的異常") def bar(): 1 / 0 gr1 = greenlet(foo) gr2 = greenlet(bar) try: gr1.switch() except Exception: print("在main中捕獲了bar的異常") """ 在main中捕獲了bar的異常 """
咱們看到當從foo切換到bar的時候,bar裏面出現了異常,可是這個異常並無拋給foo裏面,而是拋給外面。不過可能有人好奇,我怎麼沒有看到父greenlet在哪兒。剛纔說了,在咱們import greenlet的時候就已經建立了,咱們在外層開始gr1.switch的時候,是須要藉助父greenlet的力量的,所以出現了異常也要在外層的gr1.switch中捕獲。
可是,我要說可是了。其實父greenlet是能夠指定的,若是我把上面的代碼改一下。
from greenlet import greenlet def foo(): try: gr2.switch() except Exception: print("在foo中捕獲了bar的異常") raise RuntimeError("出錯啦") def bar(): 1 / 0 gr1 = greenlet(foo) gr2 = greenlet(bar, gr1) """ def __init__(self, run=None, parent=None): 建立greenlet的時候,是能夠手動指定父parent的,若是不指定那麼是main。 可是無論怎麼樣,總會有一個greenlet是main greenlet建立的 """ try: gr1.switch() except RuntimeError as e: print(e) """ 在foo中捕獲了bar的異常 出錯啦 """
咱們看到在根據bar建立greenlet的時候,咱們指定了父greenlet爲gr1,那麼bar中拋的異常就會在gr1中捕獲,至於gr1中拋出的異常,因爲它的父greenlet是main,那麼就只能在最外層捕獲了。
from greenlet import greenlet def foo(): gr2.switch() def bar(): gr1.switch() gr1 = greenlet(foo) gr2 = greenlet(bar) gr1.switch() print(f"是否死亡:{gr1.dead} {gr2.dead}") # 是否死亡:True False gr2.switch() print(f"是否死亡:{gr1.dead} {gr2.dead}") # 是否死亡:True True
咱們注意到:gr1.switch()以後,gr1已死亡,gr2未死亡。而後再gr2.switch()以後,兩個greenlet都死亡了。這是爲何,咱們來分析一下。有人以爲,在第一次gr1.switch的時候,兩個函數都應該執行完了啊,是這樣嗎?那return呢?因此對於一個greenlet來講,若是切換出去了,那麼必需要切換回來,不然這個greenlet的狀態是不會死亡的。當咱們第二次在gr2.switch的時候,此時gr2纔算是執行完畢。
那拋異常呢?
from greenlet import greenlet def bar(): raise gr2 = greenlet(bar) try: gr2.switch() except Exception: pass print(gr2.dead) # True
能夠看到拋了異常,也算是執行完畢了。由於不管是return仍是拋異常,函數的棧都會被清空。
若是對已經狀態爲dead的greenlet進行switch的話,會怎麼樣呢?
from greenlet import greenlet def foo(): print(123) def bar(): raise gr1 = greenlet(foo) gr1.switch() print(gr1.dead) gr1.switch() """ 123 True """
能夠看到沒有任何反應,可是實際上會切換到其父greenlet
greenlet提供了接口,可讓咱們使用gettrace和settrace監控整個流程。
from greenlet import greenlet, getcurrent, settrace def cb(event, args): print(f"event is {event}, args is {args}, id(args[0] is {id(args[0])}, id(args[1] is {id(args[1])})") def foo(): gr2.switch() def bar(): raise main = getcurrent() # main greenlet直接from greenlet import getcurrent便可 gr1 = greenlet(foo) gr2 = greenlet(bar) print(id(main), id(gr1), id(gr2)) # 設置回調 oldtrace = settrace(cb) try: gr1.switch() except Exception: print("error occurred") finally: settrace(oldtrace) """ 2882796614496 2882796614656 2882796614816 event is switch, args is (<greenlet.greenlet object at 0x0000029F34117360>, <greenlet.greenlet object at 0x0000029F34117400>), id(args[0] is 2882796614496, id(args[1] is 2882796614656) event is switch, args is (<greenlet.greenlet object at 0x0000029F34117400>, <greenlet.greenlet object at 0x0000029F341174A0>), id(args[0] is 2882796614656, id(args[1] is 2882796614816) event is throw, args is (<greenlet.greenlet object at 0x0000029F341174A0>, <greenlet.greenlet object at 0x0000029F34117360>), id(args[0] is 2882796614816, id(args[1] is 2882796614496) error occurred """
咱們注意到:每當switch的時候都會執行cb回調函數,第一次switch,表示從main切換到gr1,第二次表示從gr1切換到gr2,若是出異常了,那麼一樣會切換,就會從當前greenlet切換到main greenlet。cb裏面的event就是表示是switch仍是throw,args則是兩個greenlet組成的元組,表示從哪一個greenlet切換到哪一個greenlet。
greenlet建立以後,必定要結束。若是switch以後不回來了,那麼很容易形成內存泄漏
python中每一個線程都有本身的main greenlet以及對應的sub-greenlet,不一樣線程之間的greenlet是不能切換的
不可存在循環引用
from greenlet import greenlet l = [] def foo(): gr2.switch() def bar(): l.extend([x for x in range(100)]) gr1.switch() del l[:] gr1 = greenlet(foo) gr2 = greenlet(bar) gr1.switch() del gr1, gr2 print(len(l)) # 100
在bar中,給列表l添加了100個元素,可是切換出去以後再也沒有切換回來,所以del l[:]這行語句就沒法執行,容易形成內存泄漏。可是greenlet有一個機制,那就是若是greenlet實例的引用計數爲0,那麼上次掛起的地方就會拋出GreenletExit異常,咱們能夠在異常中進行資源處理。但是咱們沒有看到這個異常啊,並且咱們已經刪除了gr1和gr2了啊,由於這個異常不會拋到main greenlet,只有你捕獲纔會看到。
from greenlet import greenlet l = [] def foo(): gr2.switch() def bar(): l.extend([x for x in range(100)]) try: gr1.switch() finally: del l[:] gr1 = greenlet(foo) gr2 = greenlet(bar) gr1.switch() del gr1, gr2 print(len(l)) # 0
gevent是一個高性能網絡庫,底層使用了libev事件驅動框架,核心就是咱們上面的greenlet。原理就是將python線程轉化爲greenlet(就是咱們說的猴子補丁,將python內的線程、ssl、socket都變成了非阻塞的),那麼當greenlet遇到io操做時,好比請求網站、睡眠等待,就會切換到其餘的greenlet,等到io操做完成,再自動切換回來,這是gevent自動幫咱們完成的。
理解gevent首先要理解gevent的調度流程,在gevent當中有一個hub的概念,專門用於調度全部的greenlet實例,固然這個hub也是一個greenlet,只是特殊一些,咱們也把hub稱之爲main thread。把全部greenlet經過switch方法註冊到hub上,而後hub調度執行某一個greenlet,一旦出現io阻塞,那麼控制權就會從當前的執行的greenlet回到hub。hub再去驅動其餘的greenlet執行。
固然若是說是greenlet實際上不許確,應該是Greenlet,這個Greenlet是gevent一個類,可是繼承自greenlet.greenlet。若是繼承了,必需要重寫其內部的run方法,由於greenlet.switch的時候會使用到這個run方法,固然hub也繼承了greenlet.greenlet。
from gevent.greenlet import Greenlet """ class Greenlet(greenlet): """ from gevent.hub import Hub """ class Hub(WaitOperationsGreenlet): class WaitOperationsGreenlet(SwitchOutGreenletWithLoop): locals()['SwitchOutGreenletWithLoop'] = _greenlet_primitives.SwitchOutGreenletWithLoop class SwitchOutGreenletWithLoop(TrackedRawGreenlet): class TrackedRawGreenlet(greenlet): """
hub是Hub的實例對象,Hub最終也是集成自greenlet
import gevent def f1(): for i in range(5): print(f"f1:{i}") gevent.sleep(0) def f2(): for i in range(5): print(f"f2:{i}") gevent.sleep(0) # gevent.spawn實際上就是Greenlet.spawn,建立一個greenlet,並將該greenlet的switch()加入hub主循環回調。 t1 = gevent.spawn(f1) t2 = gevent.spawn(f2) # joinall則是開始執行,切換到hub,調度Greenlet gevent.joinall([t1, t2]) """ f1:0 f2:0 f1:1 f2:1 f1:2 f2:2 f1:3 f2:3 f1:4 f2:4 """
可是注意的是,這裏是gevent.sleep,若是是time.sleep是不會切換的。咱們須要引入一個猴子補丁
import gevent import time from gevent import monkey monkey.patch_all() def f1(): for i in range(5): print(f"f1:{i}") time.sleep(1) def f2(): for i in range(5): print(f"f2:{i}") time.sleep(1) # 傳參的話,直接spawn(f1, *args, **kwargs)便可 t1 = gevent.spawn(f1) t2 = gevent.spawn(f2) start = time.perf_counter() gevent.joinall([t1, t2]) print(time.perf_counter() - start) # 5.0046873000000005 # 總體只用了5s,說明是沒有阻塞的
固然gevent還有其餘不少用法,這裏就不看了,由於關於併發,我的仍是推薦使用asyncio,只是由於celery,因此須要研究一下eventlet,從而須要研究gevent、greenlet。