C++,python,热爱算法和机器学习
全部博文(1214)
分类: Python/Ruby
2015-01-04 22:16:38
這篇文章是要大略介紹一下coroutine和Python的相關應用的函式庫gevent,在介紹coroutine前我們先來點情境,因為目前常見的coroutine應用都是在網路程式上,因此我們得先建立一些網路框架的模形再介紹coroutine會比較容易懂
網路隨著時代發展,已經成為現代生活中越來越重要的重要的基礎,而做為提供這些服務的伺服器,負載的連線也越來越多,因此網路程式的軟體、硬體架構也一直在演變,才有辦法承載越來越多的連線數量,有人希望在一台機器能夠同時處理1萬個連線以上,所以提出,並且之後有不少新的的技術來達成這個目標,而在此我們只專注在於軟體的架構上,首先介紹最簡單的架構
這樣的網路程式非常的簡單,只有一個迴圈,單一個行程,處理完一個要求後才繼續處理下一個,理所當然這樣的效能非常差,因為連線在完成前其它的連線都無法被處理,現代的伺服器已經很少看見這樣的架構,但因為優點是簡單,如果沒有什麼大量同時連線要處理,其實這樣的架構就很足夠
因為既然單一行程只能同時處理一個請求,那很簡單的想法就是每個請求開一個行程去處理,如此一來就能同時處理更多的請求,但是這樣做有缺點,行程的copy如果是fork的話,有os paging system在成本上其實還好,但是還是有,而且越多的連線就表示需要越多的context-switch,當連線量多到一定程度時,可能大部份的CPU時間都在忙著進行context-switch,如此一來這樣的架構在此情況下是沒有效率的,但是優點是寫伺服器的部份事實上和寫單一行程阻塞式不會差太多,一樣簡單好寫
除了多行程多阻塞式,有些程式為了減少process copy的成本,或是其它的考量,會在多個行程上開多個執行緒來處理請求,也有可能是單行程多執行緒,但是基本上和上面這幾種都沒有太大的差別,而引進了執行緒帶來了一些額外的問題,dead lock、race condition等等,當不同的執行緒如果在一起有共同的東西要處理,這些常見的同作問題就會出現,使得程式得寫得更小心
為了解決上面提到多行程和多執行緒等所帶來的問題,有一種做法是只有單一主要的迴圈來檢查有無網路IO的事件發生,再來決定要怎樣處理,這樣的好處在於省掉了context-switch、process copy等等成本,也不會有dead lock、race condition等問題,但缺點在於程式的部份會變複雜,因為當你一件事件被觸發,有事情還沒做完,你就得記下目前狀態,再下次事件觸發時再依先前的狀態來決定接下來要做什麼,不像上面是線性的程式執行那樣直觀,就是這樣的網路框架
那你或許會想,有沒有可能我們能有事件驅動的好處,和阻塞式那樣的直觀好處呢? 答案或許就是Coroutine,基本上它的本質也是事件驅動,只有單一的迴圈在檢查事件的發生,但是加上了coroutine的概念,而就是這樣的函式庫
講了這麼多次coroutine,我相信大部份的讀者可能還是不懂這到底是什麼鬼東西,對於大部份程式設計師而言這應該都算是較陌生的名字,對我而言,在一開始這也是個令人困惑的名詞,但事實上只要理解以後就會發現coroutine不是這麼的難懂,用簡單的一句話來說Coroutine,就是可以暫時中斷,之後再繼續執行的程序,我們來看一個例子,事實上Python就有最基礎的Coroutine,也就是generator
# -*- coding: utf8 -*-
def foo():
for i in range(10):
# 丟資料並且把主控權交給呼叫者
yield i
print u'foo: 主控又回到我手上了,打我阿笨蛋'
bar = foo()
# 執行coroutine
print bar.next()
print u'main: 現在主控權在我們手上,做點雜事'
print 'main:hello baby!'
# 回到剛才foo這個coroutine中斷的地方繼續執行
print bar.next()
print bar.next()
執行結果:
0 main: 現在主控權在我們手上,做點雜事 main:hello baby! foo: 主控又回到我手上了,打我阿笨蛋 1 foo: 主控又回到我手上了,打我阿笨蛋 2
看到了嗎? 我們的foo在執行的過程中被中斷又繼續了好幾次,這就是coroutine,你可能覺得這樣一點用都沒有,我在一開始也這樣想,thread的context-switch也是可以暫停然後再繼續執行,所以這樣的特性好處在哪裡? 有幾個重點在於
接著我們用圖來說明coroutine和thread之間的差別
你可以發現,Coroutine所做的其實就只是在單一thread裡面不同的coroutine裡互相切換,本質上和thread很像,所以也有些coroutine叫做micro-thread,而切換通常都是我們主動去做的,看到下面這張,當你建立了不同的thread,context-switch未必是在你預期的情況下發生的,而且通常都是由OS引發的,除此之外,如果你老爸夠有錢,買了最新的Intel iCore 2000的一千核心CPU,那麼恭喜你,你的thread可能是由不同的processor執行,因此不同thread的程式真正的同時執行是有可能的
等等! 這時候你可能抓起你手邊的OS恐龍本敲你自己的頭,你這樣問道: 嘿! 恐龍本上面有提到FCFS、RR等等排程演算法,那Coroutine呢? 答案是如同我們前面提到,這由我們自己決定,這是好處之一,這時你可能又會抓著你的頭髮大叫 “網路呢? 網路呢? 講了半天我只看到Coroutine之間切來切去,我根本看不到在這裡面的半點網路成份,哪怕是一公克也好”
確實,當我一開始在讀相關資料時也很困惑,ˊ這樣跳來跳去又如何? 網路應用到底在哪裡? 現在就回到我們的主題來
到目前為止都很難和網路有什麼相關的聯想,但是想到網路就會想到IO,而事實上這樣的優點就是在於有大量IO時會顯得特別好用,我們考慮一下當遇到IO時就將主控權交給別人
Gevent的運作方式基本上就很類似這張圖,它的Coroutine是由greenlet實作的,而每個Coroutine都有一個parent,最頂層的Coroutine就是main thread或是當前的thread,每當Coroutine遇到IO的時候,就將主控權交給root coroutine,它會視哪些coroutine的IO event是已完成的,就將主控權交給他,其實就只是這樣而已,事實上程式寫起來完全和一般的阻塞式伺服器沒什麼兩樣,但是它千真萬確是非同步的,這就是它神奇的地方,我們來看點實際的例子,我們直接拿gevent範例裡的同步下載程式
#!/usr/bin/python
# Copyright (c) 2009 Denis Bilenko. See LICENSE for details.
"""Spawn multiple workers and wait for them to complete"""
urls = ['', '', '']
import gevent
from gevent import monkey
# patches stdlib (including socket and ssl modules) to cooperate with other greenlets
monkey.patch_all()
import urllib2
def print_head(url):
print 'Starting %s' % url
data = urllib2.urlopen(url).read()
print '%s: %s bytes: %r' % (url, len(data), data[:50])
jobs = [gevent.spawn(print_head, url) for url in urls]
gevent.joinall(jobs)
這程式太簡單了對吧? 但它確實能做到極高效能的同步下載,我們做一點簡單的解釋,首先令人困惑的是monkey.patch_all(),會有這行,是因為Python內建的各種函式庫裡的IO函式庫、及會阻塞住的函數,例如Sleep都會讓整個程式卡住,而不是利用Selector/epoll之類的功能來處理,所以monkey這個函式庫就是負責將Python內建的函式庫取代成以gevent的非同步形式的函式,如此一來當執行到那些IO之類的動作,會切到MainThread的coroutine進行排程,而非直接卡在那裡等結果,而當IO動作真的完成了,gevent內部會將該coroutine標示為可執行的,因此下次有機會就會排到那個coroutine,看到下面的spawn,就是在產生coroutine,在這裡的coroutine因為事實上是greenlet這個Python函式庫題供的,所以事實上叫做greenlet
你可以在腦中想像一下,spawn首先呼產生了三個執行print_head的routine ,在joinall的地方,把主控權交給第一個print_head,而在函數裡遇到了urllib2.urlopen這個IO動作,因此它將自己設為等待狀態,並且將主控權交還給MainThread,而MainThread將主控權排給了第二個print_head,同樣的也遇到了urllib IO動作,第三個也是一樣,而這三個都在等待,主控權便再次回到了MainThread,它等待有哪個gevent的IO事件完成了,就將主控權交給它,如此一來,重覆這樣的過程,三個網頁的同步下載就在coroutine的切換之間完成了,當三函數return,joinall也會結束,整個程式就跑完了
一開始會很難理解,但一但弄清楚之後就會瞭解這樣寫法的簡單和實用性,你可以忘記它背後的原理,把它當作是一般的阻塞式網路程式來寫,就會很輕鬆,但又輕量、高效能,以上大概就談到這裡,有興趣可以玩玩看gevent