OpenStack Eventlet分析(一)

轉自http://blog.csdn.net/gaoxingnengjisuan/article/details/12913275

本來打算總結一下eventlet在OpenStack中的應用,正巧在網上找到幾篇別人已經總結好的資料,而且總結的很好,這裏直接轉載過來。同時也向作者表示感謝。

    Eventlet庫在OpenStack服務中上鏡率很高,尤其是在服務的多線程和WSGI Server併發處理請求的情況下,深入瞭解eventlet庫是很必要的。Eventlet庫是由second life開源的高性能網絡庫,從Eventlet的源碼可以知道,其主要依賴於兩個關鍵的庫:

    1.greenlet

    2.select.epoll (或者epoll等類似的庫)

    greenlet庫過程了其併發的基礎,eventlet庫簡單的對其封裝之後,就構成了GreenTread。
    select庫中的epoll則是其默認的網絡通信模型。正由於這兩個庫的相對獨立性,可以從兩個方面來學習eventlet庫,首先是greenlet。

greenlet

    在介紹GreenThread之前,可以先參考下面三篇文章,對greenlet來個迅速入門。

    1.greenlet官方文檔

    2.greenlet官方文檔翻譯

    3.greentlet原理詳細介紹

    還補充一篇文檔,寫的很好。

   openstack nova基礎知識之eventlet

    通過這三篇循序漸漸的文章,大概可以瞭解到greenlet是一個稱爲協程(coroutine)的東西,有下面幾個特點。

    1.每個協程都有自己的私有stack及局部變量;

    2.同一時間內只有一個協程在運行,故無須對某些共享變量加鎖;

    3.協程之間的執行順序,完成由程序來控制;

    總之,協程就是運行在一個線程內的僞併發方式,最終只有一個協程在運行,然後程序來控制執行的順序。可以看下面的例子來理解上面的意思。

  1. import greenlet  
  2.   
  3. def test1(n):  
  4.     print "test1:",n  
  5.     gr2.switch(32)  
  6.     print "test1: over"  
  7.   
  8. def test2(n):  
  9.     print "test2:",n  
  10.     gr1.switch(23)  
  11.     print "test2: over"  
  12.   
  13. greenlet = greenlet.greenlet  
  14. current = greenlet.getcurrent()  
  15. gr1 = greenlet(test1,current)  
  16. gr2 = greenlet(test2,current)  
  17. gr1.switch(2)  
    這段程序的執行結果如下:

  1. test1: 2  
  2. test2: 32  
  3. test1: over  
    整個程序的過程很直白,首先創建兩個協程,創建的過程傳入了要執行的函數和父greenlet(在前面給出的三個鏈接中有詳細介紹),然後調用其中的一個協程的switch函數,並且傳遞參數進去,就開始執行test1,然後到了gr2.switch(32)語句,切換到test2函數來,最後又切換回去。最終test1運行結束,回到父greenlet中,執行結束。這個過程就是始終只有一個協程在運行,函數的執行流由程序自己來控制。這個過程在上面的鏈接中描述的更加具體。

GreenThread

    那麼在eventlet中對greenlet進行了簡單的封裝,就成了GreenThread,並且上面的程序還會引來一個問題,如果我們想要寫一個協程,那到底該如何來控制函數的執行過程了,如果協程多了,控制豈不是很複雜了。帶着這個問題來看eventlet的實現。

    在介紹下面的內容之前,先貼出eventlet官方的文檔,這個上面詳細的介紹了該如何來使用eventlet庫。我們從其中選出一個接口來分析。spawn函數,調用該函數,將會使用一個GreenThread來執行用戶傳入的函數。函數具體接口如下:

  1. def spawn(func, *args, **kwargs):  
    參數很清晰,想要執行的函數以及函數的參數。該函數實際上只做了三件事,最後返回創建的greenthread,因此該函數相比於spawn_n可以,得到函數調用的結果。

  1. hub = hubs.get_hub()  
  2. g = GreenThread(hub.greenlet)  
  3. hub.schedule_call_global(0,g.switch,func,args,kwargs)  
  4. return g  
    第一,我們要先知道hubs的作用,在eventlet的官方文檔有介紹,在greenlet的官方文檔開始就是我們可以自己構造greenlet的調度器,那麼hub的第一個作用就是greenthread的調度器。另外一個作用於網絡相關,所以hub有多個實現,對應於epoll,select,poll,pyevent等,我們先看前面的第一個作用。

    hub在eventlet中是一個單太實例,也也就是全局就這有這一個實例,其包含一個greenlet實例,該greenlet實例是self.greenlet = greenlet(self.run),這個實例就是官方文檔說的MAINLOOP,主循環,更加具體就是其中的run方法,是一個主循環。並且該hub還有兩個重要的列表變量,self.timers 和 self.next_timers,前者是一個列表,但是在這個列表上實現了一個最小堆,用來存儲將被調度運行的greenthread,後者,用來存儲新加入的greenthread。

    第二,創建一個GreenThread的實例,greenthread繼承於greenlet,簡單封裝了下,該類的構造函數只需要一個參數,父greenlet,然後再自己的構造函數中,調用父類greenlet的構造函數,傳遞兩個參數,GreenTread的main函數和一個greenlet的實例。第二代碼就知道,hubs中作爲MAINLOOP的greenlet是所有先創建的greenthread的父greenlet。由前面介紹greenlet的例子中,我們可以知道,當調用該greenthread的switch方法時,將會開始執行該才傳遞給父類的self.main函數。

    第三,然後單態的hub調用schedule_call_global函數,該函數的作用可以看其註釋,用來調度函數去執行。

  1. """ 
  2. Schedule a callable to be called after 'seconds' seconds have 
  3. elapsed. The timer will NOT be canceled if the current greenlet has  
  4. exited before the timer fires. 
  5. seconds: The number of seconds to wait. 
  6. cb: The callable to call after the given time. 
  7. *args: Arguments to pass to the callable when called. 
  8. **kw: Keyword arguments to pass to the callable when called. 
  9. """  
  10. t = timer.Timer(seconds, cb, *args, **kw)  
  11. self.add_timer(t)  
  12. return t  
    註釋中提到的timer是指,傳遞進來的參數會構造成Timer的實例最後添加到self.next_timer列表中。注意在spawn中傳遞進來的g.switch函數,如果調用了這個g.switch函數,則觸發了它所在的greenthread的運行。

    這三步結束之後,對spawn的調用就返回了,然而現在只是創建了一個GreenThread,還沒有調度它去執行,最後還需要再返回的結果上調用g.wait()方法,這樣就開始GreenThread的神奇之旅了。

    我們看GreenThread的wait方法的具體代碼:

  1. def __init__(self, parent):  
  2.     greenlet.greenlet.__init__(selfself.main, parent)  
  3.     self._exit_event = event.Event()  
  4.     self._resolving_links = False  
  5.   
  6. def wait(self):  
  7.     """ Returns the result of the main function of this GreenThread.  If the    
  8.     result is a normal return value, :meth:`wait` returns it.  If it raised 
  9.     an exception, :meth:`wait` will raise the same exception (though the  
  10.     stack trace will unavoidably contain some frames from within the 
  11.     greenthread module)."""  
  12.     return self._exit_event.wait()  
    wait方法調用了Event實例的wait方法,就是在這個wait函數中,調用了我們前面提到的單態實例hub的switch方法,然後該switch真正的去調用hub的self.greenlet.switch(),我們已經所過該greenlet是所有調用spwan創建的greenlet的父greenlet,該self.greenlet在初始時傳遞了一個self.run方法,就是所謂的MAINLOOP。最終,程序的運行會由於switch的調用,開始run方法中的while循環了,這是多線程開發者最熟悉的while循環了。

    在該while循環中,就對self.next_timers中的timers做處理:

  1. def prepare_timers(self):  
  2.     heappush = heapq.heappush  
  3.     t = self.timers  
  4.     for item in self.next_timers:  
  5.         if item[1].called:  
  6.             self.timers_canceled -= 1  
  7.         else:  
  8.             heappush(t, item)  
  9.     del self.next_timers[:]  
    首先處理next_timers中沒有被調用的timers,push到最小堆中去,也就是時間最小者排前面,越先被執行。然後將所有已經調用了的timer刪除掉,這是不是會有一個疑問:如果刪除了的timers沒有運行結束,那麼下次豈不是沒有機會再被調度來運行了。再看了greenthread.py中的sleep函數之後,就會明白。

    加入到heap中的timers這會按照順序開始依次遍歷,如果到了他們的執行時間點了,timer對象就會直接被調用。看下面的代碼:

  1. def fire_timers(self, when):  
  2.     t = self.timers  
  3.     heappop = heapq.heappop  
  4.     while t:  
  5.         next = t[0]  
  6.         exp = next[0]  
  7.         timer = next[1]  
  8.         if when < exp:  
  9.             break  
  10.         heappop(t)  
  11.         try:  
  12.             if timer.called:  
  13.                 self.timers_canceled -= 1  
  14.             else:  
  15.                 timer()  
  16.         except self.SYSTEM_EXCEPTIONS:  
  17.             raise  
  18.         except:  
  19.             self.squelch_timer_exception(timer, sys.exc_info())  
  20.             clear_sys_exc_info()  

    Timer對象重載了__call__方法,所以可以直接調用了,timer被調用之後,我們前面知道,傳遞進來的是g.switch,在timer中就是調用了該switch函數,直接觸動了greenthread的執行,此時,我們自定義的函數就可以被執行了。

    我們知道,如果我們自定義的函數要運行時間很長,怎麼辦,其他的greenthread則沒有機會去運行了,在openstack nova官方文檔中介紹thread中也提到這個問題,此時我們需要在自己定義的函數中調用greenthread.sleep(0)函數,來進行切換,使其他的greenthread也能被調度運行。看看greenthread.sleep函數的代碼。

  1. def sleep(seconds=0):  
  2.     """Yield control to another eligible coroutine until at least *seconds* have 
  3.     elapsed. 
  4.  
  5.     *seconds* may be specified as an integer, or a float if fractional seconds 
  6.     are desired. Calling :func:`~greenthread.sleep` with *seconds* of 0 is the 
  7.     canonical way of expressing a cooperative yield. For example, if one is 
  8.     looping over a large list performing an expensive calculation without 
  9.     calling any socket methods, it's a good idea to call ``sleep(0)`` 
  10.     occasionally; otherwise nothing else will run. 
  11.     """  
  12.     hub = hubs.get_hub()  
  13.     current = getcurrent() # 當前正在執行的greenthread,調用這個sleep函數  
  14.     assert hub.greenlet is not current, 'do not call blocking functions from the mainloop'  
  15.     timer = hub.schedule_call_global(seconds, current.switch)  
  16.     try:  
  17.         hub.switch()  
  18.     finally:  
  19.         timer.cancel()  
    從該sleep函數可以知道,我們又重新調用了一遍hub.schedule_call_global函數,然後直接調用hub.switch,這樣在運行的子greenlet中,開始觸發父greenlet(也就是MAINLOOP的greenlet)的執行,上次該greenlet正運行到 fire_timers 的timer()函數處,此時父greenlet則接着運行,開始新的調度。

    至此,調度的過程就大致描述結束了。

    greenthread中其他的函數都基本同樣,如果我們的函數只是簡單的進行CPU運行,而不涉及到IO處理,上面的知識就可以理解eventlet了,然而,eventlet是一個高性能的網絡庫,還有很大一部分是很網絡相關的。在留給下次。

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