python之gevent(3)

在之前的文章中已經(jīng)講過了gevent的使用、gevent的底層greenlet的使用以及gevent調度的源碼分析,可以閱讀文章回顧一下:python之gevent(1)python之greenlet,python之gevent(2)。本文將帶大家一起學習幾個gevent比較重要的模塊,包括Timeout,Event/AsynResult,Semphore,socket patch,這些模塊都涉及當前協(xié)程與hub的切換。

Timeout

這個類在gevent.timeout模塊,其作用是超時后在當前協(xié)程拋出異常,這樣執(zhí)行流程也強制回到了當前協(xié)程。看一個簡單的例子:

import time
import gevent
from gevent.timeout import Timeout


SLEEP = 3
TIMEOUT = 2

timeout = Timeout(TIMEOUT)
timeout.start()


def wait():
    gevent.sleep(SLEEP)
    print('log in wait')


begin = time.time()
try:
    gevent.spawn(wait).join()
except Timeout:
    print('after %s catch Timeout Exception' % (time.time() - begin))
finally:
    timeout.cancel()

運行這段代碼,輸出如下:


image.png

可以看出,在2s之后在main協(xié)程拋出了Timeout異常(繼承自BaseException)。Timeout的實現(xiàn)的核心在start函數(shù):

    def start(self):
        """Schedule the timeout."""
        if self.pending:
            raise AssertionError('%r is already started; to restart it, cancel it first' % self)

        if self.seconds is None:
            # "fake" timeout (never expires)
            return

        if self.exception is None or self.exception is False or isinstance(self.exception, string_types):
            # timeout that raises self
            throws = self
        else:
            # regular timeout with user-provided exception
            throws = self.exception

        # Make sure the timer updates the current time so that we don't
        # expire prematurely.
        self.timer.start(getcurrent().throw, throws, update=True)

從代碼中可以看到,在超時之后調用了getcurrent().throw(),throw方法會切換協(xié)程,并拋出異常(在上面的代碼中默認拋出Timeout異常)。使用Timeout有幾點需要注意:

第一:一定要記得在finally調用cancel,否則如果協(xié)程先于TIMEOUT時間恢復,之后還會拋出異常,例如下面的代碼:

import gevent
from gevent import Timeout

SLEEP = 4
TIMEOUT = 5

timeout = Timeout(TIMEOUT)
timeout.start()

def wait():
    gevent.sleep(SLEEP)
    print('log in wait')

begin = time.time()
try:
    gevent.spawn(wait).join()
except Timeout:
    print('after %s catch Timeout Exception'  % (time.time() - begin))
# finally:    
#     timeout.cancel()

gevent.sleep(2)
print 'program will finish'

#協(xié)程先于超時恢復

上述的代碼運行會拋出Timeout異常,在這個例子中,協(xié)程先于超時恢復(SLEEP < TIMEOUT),且沒有在finally中調用Timeout.cancel。最后的兩行保證程序不要過早結束退出,那么在hub調度的時候會重新拋出異常。

由于Timeout實現(xiàn)了with協(xié)議(enterexit方法),更好的寫法是將TImeout寫在with語句中,如下面的代碼:

import gevent
from gevent import Timeout

SLEEP = 4
TIMEOUT = 5


def wait():
    gevent.sleep(SLEEP)
    print('log in wait')

with Timeout(TIMEOUT):
    begin = time.time()
    try:
        gevent.spawn(wait).join()
    except Timeout:
        print('after %s catch Timeout Exception'  % (time.time() - begin))

gevent.sleep(2)
print 'program will finish'

#Timeout with

第二:Timeout只是切換到當前協(xié)程,并不會取消已經(jīng)注冊的協(xié)程(上面通過spawn發(fā)起的協(xié)程),我們改改代碼:

import gevent
from gevent import Timeout

SLEEP = 6
TIMEOUT = 5

timeout = Timeout(TIMEOUT)
timeout.start()

def wait():
    gevent.sleep(SLEEP)
    print('log in wait')

begin = time.time()
try:
    gevent.spawn(wait).join()
except Timeout:
    print('after %s catch Timeout Exception'  % (time.time() - begin))
finally:    
    timeout.cancel()

gevent.sleep(2)
print 'program will finish'
# output:
# after 5.00100016594 catch Timeout Exception
# log in wait
# program will finish

#Timeout不影響發(fā)起的協(xié)程

從輸出可以看到,即使因為超時切回了main greenlet,但spawn發(fā)起的協(xié)程并不受影響。如果希望超時取消之前發(fā)起的協(xié)程,那么可以在捕獲到異常之后調用 Greenlet.kill

第三:gevent對可能導致當前協(xié)程掛起的函數(shù)都提供了timeout參數(shù),用于在指定時間到達之后恢復被掛起的協(xié)程。在函數(shù)內部會捕獲Timeout異常,并不會拋出。例如:

SLEEP = 6
TIMEOUT = 5


def wait():
    gevent.sleep(SLEEP)
    print('log in wait')


begin = time.time()
try:
    gevent.spawn(wait).join(TIMEOUT)
except Timeout:
    print('after %s catch Timeout Exception' % (time.time() - begin))

print 'program will exit', time.time() - begin

#函數(shù)的timeout參數(shù)

Event & AsyncResult

Event用來在Greenlet之間同步,tutorial上的例子簡單明了:

import gevent
from gevent.event import Event

'''
Illustrates the use of events
'''


evt = Event()

def setter():
    '''After 3 seconds, wake all threads waiting on the value of evt'''
    print('A: Hey wait for me, I have to do something')
    gevent.sleep(3)
    print("Ok, I'm done")
    evt.set()


def waiter():
    '''After 3 seconds the get call will unblock'''
    print("I'll wait for you")
    evt.wait()  # blocking
    print("It's about time")

def main():
    gevent.joinall([
        gevent.spawn(setter),
        gevent.spawn(waiter),
        gevent.spawn(waiter),

    ])

if __name__ == '__main__': main()

Event Example

Event主要的兩個方法是set和wait:wait等待事件發(fā)生,如果事件未發(fā)生那么掛起該協(xié)程;set通知事件發(fā)生,然后hub會喚醒所有wait在該事件的協(xié)程。從輸出可知, 一次event觸發(fā)可以喚醒所有在該event上等待的協(xié)程。AsyncResult同Event類似,只不過可以在協(xié)程喚醒的時候傳值(有點類似generator的next send的區(qū)別)。接下來大致看看Event的set和wait方法。

Event.wait的核心代碼在gevent.event._AbstractLinkable._wait_core,其中_AbstractLinkable是Event的基類。_wait_core源碼如下:

def _wait_core(self, timeout, catch=Timeout):
        # The core of the wait implementation, handling
        # switching and linking. If *catch* is set to (),
        # a timeout that elapses will be allowed to be raised.
        # Returns a true value if the wait succeeded without timing out.
        switch = getcurrent().switch
        self.rawlink(switch)
        try:
            timer = Timeout._start_new_or_dummy(timeout)
            try:
                try:
                    result = self.hub.switch()
                    if result is not self: # pragma: no cover
                        raise InvalidSwitchError('Invalid switch into Event.wait(): %r' % (result, ))
                    return True
                except catch as ex:
                    if ex is not timer:
                        raise
                    # test_set_and_clear and test_timeout in test_threading
                    # rely on the exact return values, not just truthish-ness
                    return False
            finally:
                timer.cancel()
        finally:
            self.unlink(switch)

首先是將當前協(xié)程的switch加入到Event的callback列表,然后切換到hub。

接下來是set函數(shù):

    def set(self):
        self._flag = True # make event ready
        self._check_and_notify()
    def _check_and_notify(self):
        # If this object is ready to be notified, begin the process.
        if self.ready():
            if self._links and not self._notifier:
                self._notifier = self.hub.loop.run_callback(self._notify_links)

_check_and_notify函數(shù)通知hub調用_notify_links, 在這個函數(shù)中將調用Event的callback列表(記錄的是之前各個協(xié)程的switch函數(shù)),這樣就恢復了所有wait的協(xié)程。

Semaphore & Lock

Semaphore是gevent提供的信號量,實例化為Semaphore(value), value代表了可以并發(fā)的量。當value為1,就變成了互斥鎖(Lock)。Semaphore提供了兩個函數(shù),acquire(P操作)和release(V操作)。當acquire操作導致資源數(shù)量將為0之后,就會在當前協(xié)程wait,源代碼如下(gevent._semaphore.Semaphore.acquire):

def acquire(self, blocking=True, timeout=None):
        
        if self.counter > 0:
            self.counter -= 1
            return True

        if not blocking:
            return False

        timeout = self._do_wait(timeout)
        if timeout is not None:
            # Our timer expired.
            return False

        # Neither our timer no another one expired, so we blocked until
        # awoke. Therefore, the counter is ours
        self.counter -= 1
        assert self.counter >= 0
        return True

邏輯比較簡單,如果counter數(shù)量大于0,那么表示可并發(fā)。否則進入wait,_do_wait的實現(xiàn)與Event.wait十分類似,都是記錄當前協(xié)程的switch,并切換到hub。當資源足夠切換回到當前協(xié)程,此時counter一定是大于0的。由于協(xié)程的并發(fā)并不等同于線程的并發(fā),在任意時刻,一個線程內只可能有一個協(xié)程在調度,所以上面對counter的操作也不用加鎖。

Monkey-Patch

對于python這種動態(tài)語言,在運行時替換模塊、類、實例的屬性都是非常容易的。我們以patch_socket為例:

image.png

可見在patch前后,同一個名字(socket)所指向的對象是不一樣的。在python2.x環(huán)境下,patch后的socket源碼在gevent._socket2.py,如果是python3.x,那么對應的源碼在gevent._socket3.py.。至于為什么patch之后就讓原生的socket操作可以在協(xié)程之間協(xié)作,看兩個函數(shù)socket.init 和 socket.recv就明白了。

init函數(shù)(gevent._socket2.socket.init):

def __init__(self, family=AF_INET, type=SOCK_STREAM, proto=0, _sock=None):
        if _sock is None:
            self._sock = _realsocket(family, type, proto) # 原生的socket
            self.timeout = _socket.getdefaulttimeout()
        else:
            if hasattr(_sock, '_sock'):
                self._sock = _sock._sock
                self.timeout = getattr(_sock, 'timeout', False)
                if self.timeout is False:
                    self.timeout = _socket.getdefaulttimeout()
            else:
                self._sock = _sock
                self.timeout = _socket.getdefaulttimeout()
            if PYPY:
                self._sock._reuse()
        self._sock.setblocking(0) #設置成非阻塞
        fileno = self._sock.fileno()
        self.hub = get_hub()    # hub
        io = self.hub.loop.io
        self._read_event = io(fileno, 1) # 監(jiān)聽事件
        self._write_event = io(fileno, 2)

從init函數(shù)可以看到,patch后的socket還是會維護原生的socket對象,并且將原生的socket設置成非阻塞,當一個socket是非阻塞時,如果讀寫數(shù)據(jù)沒有準備好,那么會拋出EWOULDBLOCK\EAGIN異常。最后兩行注冊socket的可讀和可寫事件。再來看看recv函數(shù)(gevent._socket2.socket.recv):

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) # 如果數(shù)據(jù)準備好了,直接返回
            except error as ex:
                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()
            self._wait(self._read_event) # 等待數(shù)據(jù)可讀的watcher

如果在while循環(huán)中讀到了數(shù)據(jù),那么直接返回。但實際很大概率數(shù)據(jù)并沒有準備好,對于非阻塞socket,拋出EWOULDBLOCK異常。在最后一行,調用wait,注冊當前協(xié)程switch,并切換到hub,當read_event觸發(fā)時,表示socket可讀,這個時候就會切回當前協(xié)程,進入下一次while循環(huán)。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 在之前,我已經(jīng)在兩篇文章中分別介紹了gevent的使用以及gevent的底層greenlet的使用,可以閱讀文章回...
    WolfLC閱讀 3,374評論 0 7
  • 一. 操作系統(tǒng)概念 操作系統(tǒng)位于底層硬件與應用軟件之間的一層.工作方式: 向下管理硬件,向上提供接口.操作系統(tǒng)進行...
    月亮是我踢彎得閱讀 5,998評論 3 28
  • 進程 線程 協(xié)程 異步 并發(fā)編程(不是并行)目前有四種方式:多進程、多線程、協(xié)程和異步。 多進程編程在python...
    hugoren閱讀 4,994評論 1 4
  • 前述 進程 線程 協(xié)程 異步 并發(fā)編程(不是并行)目前有四種方式:多進程、多線程、協(xié)程和異步。 多進程編程在pyt...
    softlns閱讀 6,343評論 2 24
  • 必備的理論基礎 1.操作系統(tǒng)作用: 隱藏丑陋復雜的硬件接口,提供良好的抽象接口。 管理調度進程,并將多個進程對硬件...
    drfung閱讀 3,569評論 0 5