[gevent源碼分析] gevent兩架馬車-libev和greenlet

本篇將討論gevent的兩架馬車-libev和greenlet如何協同工作的。

gevent事件驅動底層使用了libev,我們先看看如何單獨使用gevent中的事件循環。

#coding=utf8
import socket
import gevent
from gevent.core import loop

def f():
    s, address = sock.accept()
    print address
    s.send("hello world\r\n")

loop = loop()
sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
sock.bind(("localhost",8000))
sock.listen(10)
io = loop.io(sock.fileno(),1) #1代表read
io.start(f)
loop.run()
代碼很簡單,使用core.loop新建了一個loop實例,通過io加入socket讀事件,通過start設置回調,然後run啓動事件循環,一個簡單的helloworld服務器搭建好了,可以通過telnet localhost 8000看響應結果。

gevent的整個事件循環是在hub.run中啓動的,

    def run(self):
        assert self is getcurrent(), 'Do not call Hub.run() directly'
        while True:
            loop = self.loop
            loop.error_handler = self
            try:
                loop.run()
            finally:
                loop.error_handler = None  # break the refcount cycle
            self.parent.throw(LoopExit('This operation would block forever'))
上面的self.loop和我們上面自己新建的loop對象是一樣的,下面我們通過socket的recv函數看時間是如何註冊到loop中。
gevent的socket對象被gevent重新封裝,原始socket就是下面的self._sock
我們來看看gevent的socket一次recv做了什麼操作。

gevent/socket.py

   def recv(self, *args):
        sock = self._sock  # keeping the reference so that fd is not closed during waiting
        while True:
            try:
                return sock.recv(*args) # 1.如果此時socket已經有數據,則直接return
            except error:
                #沒有數據將會拋出異常,且errno爲EWOULDBLOCK
                ex = sys.exc_info()[1]
                if ex.args[0] != EWOULDBLOCK or self.timeout == 0.0:
                    raise
                # QQQ without clearing exc_info test__refcount.test_clean_exit fails
                sys.exc_clear()
            #此時將該文件描述符的”讀事件“加入到loop中
            self._wait(self._read_event)
            """self._wait會調用hub.wait,
                def wait(self, watcher):
                    waiter = Waiter()
                    unique = object()
                    watcher.start(waiter.switch, unique) #這個watcher就是上面說的loop.io()實例,waiter.switch就是回調函數
                    try:
                        result = waiter.get()
                        assert result is unique, 'Invalid switch into %s: %r (expected %r)' % (getcurrent(), result, unique)
                    finally:
                        watcher.stop()
            當loop捕獲到”可讀事件“時,將會回調waiter.switch方法,此時將回到這裏(因爲while循環)繼續執行sock.recv(*args)
            一般來說當重新recv時肯定是可以讀到數據的,將直接返回
            """
上面的self._read_event = io(fileno, 1),再次回到while大循環中,將直接return sock.recv的結果。我們知道socke.recv(1024)可能返回的並沒有1024字節,這要看此時緩衝區已接受多少字節,所以說數據可能一次沒有讀完,所以可能會觸發多次

EWOULDBLOCK,多次讀取,只有recv爲空字符串時才代表讀取結束。典型的讀取整個數據一般如下所示:

    buff = []
    while 1:
        s = socket.recv(1024)
        if not s:
            break
        else:
            buff.append(s)
    buff = "".jon(buff)
你可能有點好奇,在gevent中有多處使用了assert判斷waiter的返回值,如:hub.wait

class Hub(greenlet):
    def wait(self, watcher):
        waiter = Waiter()
        unique = object()
        watcher.start(waiter.switch, unique)
        try:
            result = waiter.get()
            assert result is unique, 'Invalid switch into %s: %r (expected %r)' % (getcurrent(), result, unique)
            #這裏爲什麼要assert?
            #因爲正常肯定是loop調用waiter.switch(unique),那麼waiter.get()獲取的肯定是unique,
            #如果不是unique,肯定是有其它地方調用waiter.switch,這很不正常
        finally:
            watcher.stop()

這主要是爲了防止回調函數被其它greenlet調用,因爲greenlet通過switch傳遞參數,看下面代碼:

def f(t):
    gevent.sleep(t)

p = gevent.spawn(f,2)
gevent.sleep(0) # 2s後libev將回調f,所以下面p.get獲取的是2 
switcher = gevent.spawn(p.switch, 'hello') #強先回調p.switch,傳遞參數hello
result = p.get()
將返回以下異常:

將報如下異常:
AssertionError: Invalid switch into <Greenlet at 0x252c2b0: f(2)>: 'hello' (expected <object object at 0x020414E0>)
<Greenlet at 0x252c2b0: f(2)> failed with AssertionError

我們再看看gevent封裝的greenlet,

class Greenlet(greenlet):
    """A light-weight cooperatively-scheduled execution unit."""

    def __init__(self, run=None, *args, **kwargs):
        hub = get_hub()
        greenlet.__init__(self, parent=hub)
        if run is not None:
            self._run = run
我們看到所有的Greenlet的parent都是hub,這有什麼好處呢?
因爲當一個greenlet死掉的時候將回到父greenlet中,也就是hub中,hub將從運行上次回調的地方繼續開始事件循環,這也就是爲什麼事件循環是在hub中運行的理由。

我們來看一個一個Greenlet的生命週期

啓動Greenlet需要調用start()方法,

    def start(self):
        """Schedule the greenlet to run in this loop iteration"""
        if self._start_event is None:
            self._start_event = self.parent.loop.run_callback(self.switch)
也就是將當前的switch加入到loop事件循環中。當loop回調self.switch時將運行run方法(這是底層greenlet提供的),

繼承時我們可以提供_run方法。

    def run(self):
        try:
            if self._start_event is None:
                self._start_event = _dummy_event
            else:
                self._start_event.stop() #取消之前添加的回調函數,loop將會從回調鏈中剔除該函數。
                #libev提供了一系列的對象封裝,如io,timer,都有start,stop方法
                #而回調是通過loop.run_callback開啓的,和其它有所不同
            try:
                result = self._run(*self.args, **self.kwargs) #運行自定義_run方法
            except:
                self._report_error(sys.exc_info())
                return
            self._report_result(result) #設置返回結果,這是個比較重要的方法,下面會單獨看看
        finally:
            pass
一切順利,沒有異常將調用_report_result方法,我們具體看看:
    def _report_result(self, result):
        self._exception = None
        self.value = result #設置返回結果,可通過get()獲取,注意要獲取value時
        #不要直接通過.value,一定要用get方法,因爲get()會獲取到真正的運行後結果,
        #而.value那是該Greenlet可能還沒結束
        if self._links and not self._notifier: #這個是幹什麼的?
            self._notifier = self.parent.loop.run_callback(self._notify_links)
爲什麼說一定要通過get()才能獲取最後返回結果呢,因爲get()相當於異步的結果返回,那麼很有可能Greenlet還沒結果我們就調用
get()想獲取結果,如果不是異步,肯定是獲取不到的。我們看看get()操作,

    def get(self, block=True, timeout=None):
        """Return the result the greenlet has returned or re-raise the exception it has raised.

        If block is ``False``, raise :class:`gevent.Timeout` if the greenlet is still alive.
        If block is ``True``, unschedule the current greenlet until the result is available
        or the timeout expires. In the latter case, :class:`gevent.Timeout` is raised.
        """
        if self.ready(): #該Greenlet已經運行結束,直接返回結果
            if self.successful():
                return self.value
            else:
                raise self._exception
        if block: #到這裏說明該Greenlet並沒有結束
            switch = getcurrent().switch
            self.rawlink(switch) #將當前Greenlet.switch加到自己的回調鏈中
            """
            self._links.append(callback)
            """
            try:
                t = Timeout.start_new(timeout)
                try:
                    result = self.parent.switch() #切換到hub,可以理解爲當前get()阻塞了,當再次回調剛剛註冊的switch將回到這裏
                    #可問題是好像我們沒有將switch註冊到hub中,那是誰去回調的呢?
                    #幕後黑手其實就是上面的_report_result,當Greenlet結束最後會調用_report_result,
                    #而_report_result把將_notify_links註冊到loop的回調中,最後由_notify_links回調我們剛註冊的switch
                    # def _notify_links(self):
                    #     while self._links:
                    #     link = self._links.popleft()
                    #     try:
                    #         link(self) #就是這裏了,我們看到還把self傳給了switch,所以result結果就是self(greenlet通過switch傳遞結果)
                    #     except:
                    #         self.parent.handle_error((link, self), *sys.exc_info())
                    assert result is self, 'Invalid switch into Greenlet.get(): %r' % (result, ) 
                    #知道爲什麼result是self的原因了吧
                finally:
                    t.cancel()
            except:
                self.unlink(switch)
                raise
            #運行到這裏,其實Greenlet已經結束了,換句話說self.ready()肯定爲True
            if self.ready():
                if self.successful():
                    return self.value
                else:
                    raise self._exception
        else: #還沒結束,你又不等待,沒有值返回啊,只能拋出異常了
            raise Timeout

通過上面我們知道其實get()就是異步返回結果的方式,當Greenelt要結束時通過run()函數最後的_report_result返回,所以_report_result還是很重要的。

其實_notify_links不只爲get提供了最後回調的方法,還提供了Grenlet的link協議。所謂link協議就是Greenlet可以通過

link方法把執行結果傳遞給一回調函數。

def f(source):
    print source.value
gevent.spawn(lambda: 'gg').link(f)
gevent.sleep(1)
當Greenlet結束時就會調用f方法,並把self傳給f。AsyncResult通過__callback__提供了link方法。
from gevent.event import AsyncResult
a = AsyncResult()
gevent.spawn(lambda: 'gg').link(a)
print a.get()
gevent.sleep(1)
看看AsyncEvent的__call__方法,和我們上面的f差不多

    # link protocol
    def __call__(self, source):
        if source.successful():
            self.set(source.value)
        else:
            self.set_exception(source.exception)

其實Greenlet還提供了一個switch_out的方法,在gevent中switch_out是和switch相對應的一個概念,當切換到Greenlet時將

調用switch方法,切換到hub時將調用Greenlet的switch_out方法,也就是給Greenlet一個保存恢復的功能。

gevent中backdoor.py(提供了一個python解釋器的後門)使用了switch,我們來看看

class SocketConsole(Greenlet):

    def switch(self, *args, **kw):
        self.saved = sys.stdin, sys.stderr, sys.stdout
        sys.stdin = sys.stdout = sys.stderr = self.desc
        Greenlet.switch(self, *args, **kw)

    def switch_out(self):
        sys.stdin, sys.stderr, sys.stdout = self.saved

switch_out用的非常漂亮,因爲交換環境需要使用sys.stdin,sys.stdout,sys.stderr,所以當切換到我們Greenlet時,

把這三個變量都替換成我們自己的socket描述符,但當要切換到hub時需要恢復這三個變量,所以在switch中先保存,在switch_out中再恢復,switch_out是切換到hub時,與hub的switch調用實現:

class Hub(Greenlet):
    def switch(self):
        #我們看到的確是先調用先前的Greenlet.switch_out
        switch_out = getattr(getcurrent(), 'switch_out', None)
        if switch_out is not None:
            switch_out()
        return greenlet.switch(self)
可以通過下面兩句話就啓動一個python後門解釋器,感興趣的童鞋可以玩玩。
from gevent.backdoor import BackdoorServer
BackdoorServer(('127.0.0.1', 9000)).serve_forever()

通過telnet,你可以爲所欲爲。

在gevent中基本上每個函數都有timeout參數,這主要是通過libev的timer實現。

使用如下:

Timeout對象有pending屬性,判斷是是否還未運行

t=Timeout(1)
t.start()
try:
    print 'aaa'
    import time
    assert t.pending == True
    time.sleep(2)
    gevent.sleep(0.1) 
    #注意這裏不可以是sleep(0),雖然sleep(0)也切換到hub,定時器也到了,但gevent註冊的回調
    #是優先級是高於定時器的(在libev事件循環中先調用callback,然後纔是timer)
except Timeout,e:
    assert t.pending == False
    assert e is t #判斷是否是我的定時器,和上面的assert一致,防止不是hub調用t.switch
    print sys.exc_info()
finally: #取消定時器,不管定時器是否可用,都可取消
    t.cancel()
Timout對象還提供了with上下文支持:

with Timeout(1) as t:
    assert t.pending
    gevent.sleep(0.5)
assert not t.pending
Timeout第二個參數可以自定義異常,如果是Fasle,with上下文將不傳遞異常
with Timeout(1,False) as t:
    assert t.pending
    gevent.sleep(2)
assert not sys.exc_info()[1]
我們看到並沒有拋出異常

還有一個with_timeout快捷方式:

def f():
    import time
    time.sleep(2)
    gevent.sleep(0.1) #不能使用gevent.sleep(0)
    print 'fff'

t = with_timeout(1,f,timeout_value=10)
assert t == 10
注意with_timeout必須有timeout_value參數時纔不會拋Timeout異常。

到這裏我們對gevnet的底層應該都很熟悉了,對gevent還未介紹到的就是一些高層的東西,如Event,Pool等,後期也會單獨拿出來

講講。我覺得還需要關注的就是libev的使用,不過這就需要我們深入分析core.pyx的libev cython擴展了,這需要cython的知識,最近我也一直在看源碼,後期也會和大家分享。

至於爲什麼要分析libev的擴展呢?主要是在遊戲中有一些定時執行的任務,通過gevent現有的實現比較蹩腳,其實libev提供的timer有兩個參數,一個after,一個repeat,after是多久以後啓動該定時器,repeat是多次以後再次啓動,這剛好滿足我的需求,

下面就是我寫的一個簡單的定時任務腳本,通過gfirefly啓動,還提供了web接口。

#coding:utf-8
'''
Created on 2014-9-5

@author: http://blog.csdn.net/yueguanghaidao
'''
import traceback
import datetime
from flask import request
from gevent.hub import get_hub
from gtwisted.utils import log
from gfirefly.server.globalobject import webserviceHandle
from app.models.role import Role

'''
定時任務

任務名 (運行時間(0-24),每次間隔)單位爲小時,回調函數均爲do_name
'''

CRONTAB = {
    "energy": (0, 1), #恢復體力
    "god_surplustime": (0, 24),
    "arena_surplustime": (22, 24),
    "arena_rewrad": (21, 24),
    "sign_reset": (1, 24)
}


def log_except(fun):
    def wrapper(*args):
        try:
            log.msg(fun.__name__)
            return fun(args)
        except:
            log.msg(traceback.format_exc())
    return wrapper

class Task(object):
    """所有定時任務
    """
    @classmethod
    @log_except
    def do_energy(cls):
        """每一個小時增加1體力(體力小於8)
        """
        Role.objects(energy__lt=8).update(inc__energy=1)

    @classmethod
    @log_except
    def do_god_surplustime(cls):
        """財神剩餘次數
        """
        Role.objects(god__exists=True).update(set__god__surplustime=10)

@webserviceHandle("/cron", methods=['GET', 'POST'])
def cron():
    """提供web接口調用
    """
    action = request.args.get("action")
    if not action:
        return "action:<br/><br/>"+"<br/>".join(( a for a in CRONTAB))
    else:
        try:
            f = getattr(Task, "do_"+action)
            try:
                f()
            except:
                return traceback.format_exc()
            return "success"
        except AttributeError:
            return "action:<br/><br/>"+"<br/>".join(( a for a in CRONTAB))

def timer(after, repeat):
    return get_hub().loop.timer(after, repeat)

def run():
    log.msg("cron start")
    #配置mongodb
    mongoconfig.init_Mongo()

    for action, t in CRONTAB.items():
        log.msg("%s start" % action)
        f = getattr(Task, "do_"+action)
        now = datetime.datetime.now()
        other = now.replace(hour=t[0],minute=0,second=0)
        if other > now:
            after = (other-now).seconds
        else:
            after = 24*3600-(now-other).seconds
        #after = t[0]*3600
        timer(after, t[1]*3600).start(f)

run()




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章