openstack開發實踐(八):基於協程的多線程技術在openstack中的應用

協程簡介

Python相比較C++等其他編程語言,由於全局解釋鎖(GIL)的存在,在同一時刻只能有一條線程可以向前進行,無法利用多線程進行並行計算,因此Python無法很方便地實現多線程。在Python的世界裏,要想實現多線程的功能,需要使用協程,所謂協程,又叫綠色線程,是一個用戶態的輕量級的線程,它的調度完全可以由用戶控制。從本質上講,協程可以說是用戶態的線程。每個協程都有自己的寄存器上下文和棧,我們在編寫協程時,可以顯式地控制協程的切換時機,協程調度切換時,將寄存器上下文和棧保存到其他地方,在切回來的時候,恢復先前保存的寄存器上下文。相比較進程和線程來說,協程的調度完全由用戶控制(進程和線程的調度由操作系統完成),協程擁有自己的寄存器上下文和棧,協程間切換的開銷相比較進程來說,要小了很多。

eventlet庫簡介

eventlet依賴的兩個庫:greenlet和select.epoll

openstack作爲一個開源的雲平臺,其設計架構中天然支持對高併發請求的處理,而它自身的高併發,正是藉助於eventlet庫通過協程的併發來實現的。eventlet是python的一個用來處理和網絡相關的函數庫,主要依賴greenlet和select.epoll這兩個庫,前者主要實現的是併發控制,後者主要是網絡模型。首先來簡單介紹一下greenlet的例子,這個例子來源於greenlet官方文檔https://greenlet.readthedocs.io/en/latest/,主要通過greenlet()定義了兩個協程,然後通過switch()來實現協程之間的切換。最後一行gr1.switch()跳到test1,打印12,跳到test2,打印56,跳回到test1,打印34; 然後test1完成並gr1死亡。 此時,執行返回到原始的gr1.switch()調用,注意到78不會被打印。

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()

greenlet的一個最大的缺點,就是需要手動顯示切換,對於openstack這樣一個大型項目來說,每進行一次切換都需要顯式調用switch()函數會過於繁瑣。所以在eventlet中將greenlet封裝爲GreenThread,而GreenThread的切換調度則通過Hub來實現。下圖爲將greenlet封裝爲GreenThread的代碼(/usr/local/lib/python2.7/dist-packages/eventlet/greenthread.py)
在這裏插入圖片描述
對於GreenThread的調度器hub來說,hub是eventlet中所有事件的監聽和處理中心,也是greenlet的調度中心,它可以調度io事件和greenlet。hub調度的,其實是一個個Timer, 對於任何傳入到hub的函數,首先就會封裝成Timer,代表了該函數將會在多久之後被執行,該函數對於greenlet調度來說就是switch()協程切換函數,hub對於Timer的調度過程的代碼在/usr/local/lib/python2.7/dist-packags/eventlet/hubs/hub.py的run函數中,如下圖所示,其中第333行self.prepare_timers()將timer按到期時間進行排序,第336行self.fire_timers(self.clock())觸發定時事件,第340行self.sleep_until()計算快到期的定時事件時間戳,第346行self.wait()轉交執行權到被調度的Timer事件,由Hub調用下一個需要運行的GreenThread。
在這裏插入圖片描述
關於epoll庫,它可以看作一個Hub的實例,用來處理和網絡相關的東西。

創建協程和協程控制的常用API

在eventlet代碼中,有幾個比較常用的創建協程和協程控制的API,如下所示:

  1. eventlet.spawn(func, *args, **kwargs):創建一個協程去執行func,該func會以args和kwargs作爲參數,spawn()的返回值是一個eventlet.GreenThread對象。如果綠色線程池沒有被佔滿,這個綠色線程創建後就會被立即執行。
  2. eventlet.spawn_n(func, *args, **kwargs):和spawn()類似,不過沒有返回值
  3. eventlet.spawn_after(seconds, func, *args, **kwargs):與spawn()類似,它需要在協程創建之後seconds秒之後執行
  4. eventlet.sleep(seconds=0):終止當前正在執行的綠色線程並轉交控制權
  5. class.eventlet.GreenPool:這是一個與協程有關的類,該類在初始化時可以指定協程池的大小,在這個類中定義了spawn()來創建綠色線程,定義了running()來返回當前池中綠色線程的數量等。
  6. class.eventlet.GreenPile:維護了一個GreenPool對象和一個Queue對象,前者用於創建協程,後者用於存儲spawn()方法的返回值。
    下面我們通過分析spawn()函數來進一步研究協程創建的具體過程。spawn()函數的代碼位於/usr/local/lib/python2.7/dist-packages/eventlet/greenthread.py中,如下圖所示,hub之前介紹過爲greenlet的調度器,第49行get_hub()返回噹噹前greenlet的hub的一個單態實例,第50行創建一個GreenThread實例,繼承了Hubs中作爲MAINLOOP調度過程的greenlet,當調用該實例的switch方法時,hub.greenlet開始執行,開始調度過程。
    在這裏插入圖片描述
    第51行的schedule_call_global函數的具體作用可以看代碼中的註釋(代碼位於/usr/local/lib/python2.7/dist-packags/eventlet/hubs/hub.py),如下圖所示,將g.switch作爲timer加入到hub的調度列表中(self.add_timer()),由hub調度Timer的運行,具體調度運行的過程見上文的/usr/local/lib/python2.7/dist-packags/eventlet/hubs/hub.py中的run函數。
    在這裏插入圖片描述

nova-api啓動過程中eventlet應用示例

下面我們以nova-api服務啓動過程爲例看一下eventlet的應用,下面是
啓動nova-api服務時調用的nova.cmd.api中的main()方法,第60行的launch_service()啓動多個WSGI Server在這裏插入圖片描述
launch_service()的具體代碼如下所示,其中第534行self._start_child()啓動多個子進程,具體_start_child()代碼如下所示,通過第500行os.fork()調用一個進程,子進程內(pid==0)調用_child_process
在這裏插入圖片描述
在這裏插入圖片描述
_child_process的代碼如下所示,子進程中需要啓動一個協程來監視父進程是否關閉管道,首先需要使用第472行eventlet.hubs.use_hub(hub=None)來配置使用的hub,之後第477行創建一個協程來監視父進程是否關閉管道,接下來子進程內也創建一個Launcher,調用Laucher.launch_service方法。調用ServiceLauncher中的launch_service方法(上一個launch_service爲ProcessLauncher中的方法,ServiceLauncher和ProcessLauncher的區別是ServiceLauncher用來啓動單進程的服務,而ProcessLauncher用來啓動有多個worker子進程的服務),將服務啓動。
在這裏插入圖片描述

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