inlineCallbacks: A New Way towards Asynchronous Programming

異步編程,是目前解決性能問題的一個大方向。其中怎麼樣實現異步有多種不同的實現方式。通過異步的方式,能夠實現更高的資源利用和響應性。在網絡和圖形界面編程裏面,一種非常普遍的做法是基於事件來實現用戶響應性。也就是程序利用一個主事件循環,不斷的處理觸發的事件。而對應事件的處理是通過回調(callback)的形式註冊到事件循環中,當對應的事件觸發的時候,主循環就是調用對應的回調。

雖然這種基於事件和回調的編程模式存在了很多年了,但是用回調來寫業務邏輯有一種很不爽的感覺,那就是經常的發事件,然後寫對應的回調函數,會將一個很簡單的處理邏輯分散在不同的地方,並且很有可能會引入額外的複雜性。自己在寫界面的時候就經常出現一段緊密相關的邏輯分佈在兩個不同的類中,使得在找對應的上下文的時候出現極大的阻礙。

對於這種情況,在Python裏面的twisted.defer提供了一種很優雅的解決方案。利用defer裏面的inlineCallbacks這個decorator,可以使我們寫異步的代碼可以像寫同步的代碼一樣,從而降低了異步編程的難度。(在C# 5和Javascript的Jscex裏面已經有類似的實現)

twisted是一個Python的基於事件循環的網絡庫,裏面實現了基本的事件循環和各種相關的網絡工具。其中的defer抽象就是這篇文章主要介紹的對象。關於twisted的介紹可以看官網的教程,或者是著名的poetry twisted tutor

例子

本文會用一個比較典型的例子來進行講解。想象我們需要寫這麼一個服務器:

一個視頻下載服務器,在接受到客戶端的請求之後,會去下載相關的視頻,並保存在服務器本地。具體來說,客戶段會發送給服務器一個段地址。服務器在接受到短地址之後,會首先向段地址服務提供商請求轉換段地址。在服務器接受到轉換後的原地址之後,會向真正的下載地址發出真正的下載請求,然後在下載完成之後,將它保存起來。

首先,服務器程序肯定不會是同步的去處理這種請求,因爲這樣就大大的降低服務器的處理能力。所以我們會用異步調用的方式來處理這個請求,而在twisted裏面就是通過註冊事件回調的方式來完成。

同步實現

假設我們利用同步的方式來完成上述的功能,對應的代碼應該是像下面這樣:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def stringReceived(self, shortUrl):
    self.transport.loseConnection()
    self.downloadVideoFromShortUrl(shortUrl)

def downloadVideoFromShortUrl(self, shortUrl):
    try:
        url = transformShortUrl(shortUrl)
        video = downloadVideoFromUrl(url)
        storeVideo(video)
    except BaseException, e:
        print "exception:", e

其中,stringReceived函數會在接收到客戶端發送過來的短地址之後調用,參數就是對應的shortUrl。在downloadVideoFromShortUrl裏面的是程序的主要邏輯,它按順序的調用了shortUrl轉換、從url下載地址視頻和本地儲存視頻文件。假設每個函數都是同步調用的話,邏輯非常清晰,看代碼的時候直接從上往下讀就可以了。其中也包含了錯誤的處理,也就是一個大的try…catch,其中transformShortUrldownloadVideoFromUrl會在出現錯誤的時候拋BaseException

但是同步代碼的問題就在於,當你進程阻塞在任何一個同步調用上的時候,你的進程什麼都幹不了了。所以這個時候我們就會利用異步調用來解決這個問題。假設transformShortUrldownloadVideoFromUrl都變成了異步調用。一般來說異步調用的結果我們都會通過回調的方式來處理。現在看看代碼是怎麼樣。

基於回調的異步實現

基本的代碼如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
def downloadVideoFromShortUrlAsync(self, shortUrl):
    d = transformShortUrlAsync(shortUrl)

    def downloadVideoFromUrl(url):
        print "long url:", url
        d = downloadVideoFromUrlAsync(url)

        def errDownloadVideoFromUrl(err):
            print "exception:", err

        d.addCallbacks(storeVideo, errDownloadVideoFromUrl)

    def errTransformShortUrl(err):
        print "exception:", err

    d.addCallbacks(downloadVideoFromUrl, errTransformShortUrl)

爲了容易區別,我把所有異步調用的函數都在函數名後面加上Async,來表示它是一個異步調用。每個異步調用會返回一個defer,暫且你可以認爲這個defer表示的是這個調用是異步的。當你要處理這個異步調用的結果的時候,就往這個defer上面添加一個函數。當這個異步調用完成之後,就會調用添加到這個defer上面的函數。

由於現在我們要用回調來處理調用結果,所以我們就要將處理結果的邏輯放在另一個函數裏面。就比如我們在轉換完段地址之後,會從這個地址下載視頻。而下載視頻的邏輯就另外定義一個函數來完成,也就是代碼中的downloadVideoFromUrl。可以看到,處理邏輯已經變得複雜,而且增加了嵌套。況且處理的邏輯有點不符合從上往下的閱讀習慣。在利用回調的實現裏面,必須將結果的處理和調用邏輯分開寫,否則你無法完成操作。在寫一些帶有循環和複雜邏輯的代碼的時候,這個弊端就會顯現出來。

而且你可以看到處理錯誤的邏輯和正確的處理邏輯被分割開,你很難看出裏面的具體邏輯。如果你不是寫習慣了這種基於回調的代碼,相信一般人很難在一開始的時候就看出上面的邏輯。

既然基於回調的寫程序方式那麼的反人類,那麼我們有什麼解決方案呢?twisted的inlineCallbacks就出場了。

基於inlineCallbacks的異步實現

首先我們的幾個基本調用還是異步,那麼用了inlineCallbacks之後的代碼如下:

1
2
3
4
5
6
7
8
@inlineCallbacks
def downloadVideoFromShortUrlAsync(self, shortUrl):
    try:
        url = yield transformShortUrlAsync(shortUrl)
        video = yield downloadVideoFromUrlAsync(url)
        storeVideo(video)
    except BaseException, e:
        print "exception:", e

省略掉多出來的yield,這個代碼就和同步的一模一樣!!唯一不同的就是在異步調用的前面加上了yield!!

怎麼樣,這樣寫代碼是不是很爽?

但是細想一下,我們的transformShortUrlAsync明明是異步調用啊,明明不能馬上的獲得結果啊,那url = transformShortUrlAsync那不就是錯誤的麼?

祕密就在於我們多加上去的inlineCallbacks這個decoratoryield上面。首先解釋一下,downloadVideoFromShortUrlAsync本身也是一個異步調用。當他執行到第一個異步調用的地方,它會在yield的地方“等待”異步調用的執行結束和返回結果。在第二個異步調用的地方也是同樣的,他也是“等待”異步調用的執行結束和返回結果。

也就是從downloadVideoFromShortUrlAsync的角度來說,他的執行順序是和同步沒有差別的,他也是首先執行transformShortUrl,然後downloadVideo,最後store。而且從代碼的結構上來說,也是很清晰的反應出了這一點。

但是,你會不會覺得這裏有點怪怪的?既然downloadVideoFromShortUrlAsync函數會在yield的地方等待異步調用的執行,那麼整個調用本身不就又變回同步的了麼?那我用異步調用來幹什麼……

神奇就神奇在,如果yield後面的函數調用是異步的,那麼downloadVideoFromShortUrlAsync也還是異步的!但是他要等待結果,怎麼異步啊?其實,整個函數的執行是這樣的:

  1. 進入downloadVideoFromShortUrlAsync函數,調用transformShortUrlAsync
  2. 由於transformShortUrlAsync是一個異步調用,所以在函數返回的時候,結果還沒有產生。這個時候,downloadVideoFromShortUrlAsync就返回了。
  3. transformShortUrlAsync的結果產生之後,就會繼續從downloadVideoFromShortUrlAsync函數沒有執行的部分開始執行,這個時候url就獲得了異步調用的結果。
  4. 接着調用downloadVideoFromUrlAsync,和step 2一樣,當這個異步調用返回的時候,downloadVideoFromShortUrlAsync就又返回了。
  5. transformShortUrlAsync的結果獲得之後,執行就又從downloadVideoFromShortUrlAsync沒有執行的部分開始執行,這個時候就video就賦值爲已經下載的視頻文件了。
  6. 接着執行餘下的部分。

整個執行時序就如下面這幅圖顯示:

sequence diagram of downloadVideoFromShortUrl

就如上面的圖顯示的這樣,downloadVideoFromShortUrlAsync會在異步調用的結果返回之後繼續調用接下來的部分。

需要注意的是,inlineCallbacks並不會將一個本來同步的函數變成異步,他只是使得一個函數在調用異步函數的時候可以很方便的書寫,並且將自己也變成一個異步函數。但是如果你調用的函數不是異步的,那麼用inlineCallbacks修飾的這個函數也不會是異步的。

inlineCallbacks的實現

所以我們最關心的是,How does the magic happen? 那我們直接來看看代碼實現。注意這裏我假設你知道Python的decorator, 也知道Python的generator。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
def inlineCallbacks(f):
    def unwindGenerator(*args, **kwargs):
        try:
            gen = f(*args, **kwargs)
        except _DefGen_Return:
            raise TypeError(
                "inlineCallbacks requires %r to produce a generator; instead"
                "caught returnValue being used in a non-generator" % (f,))
        if not isinstance(gen, types.GeneratorType):
            raise TypeError(
                "inlineCallbacks requires %r to produce a generator; "
                "instead got %r" % (f, gen))
        return _inlineCallbacks(None, gen, Deferred())
    return mergeFunctionMetadata(f, unwindGenerator)

其中的mergeFunctionMetaData其實就是將f的__name__和__doc__賦給unwindGenerator。而我們從unwindGenerator可以看到,函數首先調用了f,也就是被修飾的函數,而因爲要用inlineCallbacks的函數一般都是generator,這個函數返回的是一個generator object。所以最重要的函數是_inlineCallbacks這個函數。我們再來看看它的實現。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
def _inlineCallbacks(result, g, deferred):
    waiting = [True, # waiting for result?
               None] # result

    while 1:
        try:
            isFailure = isinstance(result, failure.Failure)
            if isFailure:
                result = result.throwExceptionIntoGenerator(g)
            else:
                result = g.send(result)
        except StopIteration:
            # fell off the end, or "return" statement
            deferred.callback(None)
            return deferred
        except _DefGen_Return, e:
            appCodeTrace = exc_info()[2].tb_next
            if isFailure:
                appCodeTrace = appCodeTrace.tb_next
            if appCodeTrace.tb_next.tb_next:
                ultimateTrace = appCodeTrace
                while ultimateTrace.tb_next.tb_next:
                    ultimateTrace = ultimateTrace.tb_next
                filename = ultimateTrace.tb_frame.f_code.co_filename
                lineno = ultimateTrace.tb_lineno
                warnings.warn_explicit(
                    "returnValue() in %r causing %r to exit: "
                    "returnValue should only be invoked by functions decorated "
                    "with inlineCallbacks" % (
                        ultimateTrace.tb_frame.f_code.co_name,
                        appCodeTrace.tb_frame.f_code.co_name),
                    DeprecationWarning, filename, lineno)
            deferred.callback(e.value)
            return deferred
        except:
            deferred.errback()
            return deferred

        if isinstance(result, Deferred):
            # a deferred was yielded, get the result.
            def gotResult(r):
                if waiting[]:
                    waiting[] = False
                    waiting[1] = r
                else:
                    _inlineCallbacks(r, g, deferred)

            result.addBoth(gotResult)
            if waiting[]:
                waiting[] = False
                return deferred

            result = waiting[1]

            waiting[] = True
            waiting[1] = None

    return deferred

首先知道,_inlineCallbacks這個函數的3個參數接受的分別是上一次這個generator返回的結果(result),這個generator(g),還有這個generator對應的defer(deferred)。

首先,這個函數第一次調用是從inlineCallbacks(注意區分有沒有下劃線開頭)裏面調過來的。所以第一次調用的時候,result是None,而g是一個開沒有開始執行的generator。

而最重要的就是7-11行的代碼。

  1. 首先7行的代碼就是取得result的類型信息。這樣需要注意的是,如果異步調用返回的是一個錯誤的結果,那麼類型就是failure.Failure。如果是正常的話,就不是failure.Failure
  2. 8-11行:接着就根據result的類型來進行不同的處理。如果result是failure的話,那麼就調用result.throwExceptionIntoGenerator(g),這個函數的作用就是將result對應的異常拋進g裏面。
    如果result的類型不是failure的話,那麼就是正常的結果。所以就直接用g.send(result)來將結果傳進這個generator裏面。注意到,當第一次調用_inlineCallbacks的時候,result是None,所以第一次調用相當於調用下面的代碼:g.send(None)。這個用法是正確的,因爲當generator還沒有開始的時候,g.send()只能傳None這樣的參數。

接下來最重要的就是39到46行的代碼。注意到上面對generator的操作會返回一個這個yield的值。如果yield出來的一個defer,那麼表示這個時候yield後面跟的是一個異步調用,所以這個時候,_inlineCallbacks會將一個gotResult函數傳進這個defer裏面,這樣當異步調用完成的時候,gotResult就會被調用並處理調用的結果。

在gotResult裏面,忽略掉if waiting那一段,其實最後的就是調用回_inlineCallback自己。所以現在我們大概可以有下面一個執行順序了:

當我們調用downloadVideoFromShortUrlAsync的時候,最開始的時候是在inlineCallbacks的裏面調用了一次這個函數,而一個generator在開始的時候是直接返回一個generator object的。這個時候inlineCallbacks就調用了_inlineCallbacks(None, gen, Deferred())

這時進到_inlineCallbacks裏面的時候就會走到11行,就是result = g.send(None)。這個語句是成立的。這個時候downloadVideoFromShortUrlAsync就開始運行,直到調用到transformShortUrlAsync並且返回一個defer。這個時候就繼續走到48行。也就是在這個defer上面添加gotResult函數。那麼當這個defer被調用(也就是結果獲得)的時候,gotResult就會獲得這個結果,並繼續執行downloadVideoFromShortUrlAsync下面的代碼。

分析

正如前面所講,有了inlineCallbacks之後,其實自己定義的函數並沒有變成異步,只不過他將函數裏面調用異步函數的地方自動的做了回調的處理,從而使得函數本身以一種“奇怪”的方式異步執行。

爲什麼可以有這種效果呢?我覺得主要有以下幾點:

  1. AIO,也就是異步IO。這個可以說是實現這種語法結果的必要條件,因爲當我們從調用異步函數的地方獲得了一個defer之後,這時候並沒有獲得結果。而結果會在未來的某個時刻獲得。而我們需要在獲得結果的那個時刻,函數餘下的部分可以繼續執行,而這一個就是AIO的用法,我們就可以把獲得結果的處理部分當做回調那樣傳遞給這個IO操作,讓他自動的在操作完成的時候調用這個回調。而在twisted裏面,AIO的是使用事件循環來實現的。
  2. generator。這個並不是實現inlineCallbacks這種語法結構的必要條件,就像Jscex裏面就是通過修改語法樹的方式來實現,因爲Javascript裏面是沒有generator的。但是有了generator之後,就會發現實現這個結構會異常的簡單,就像本身就應該是這麼寫的一樣。可以說generator對於基於回調的一些實現都是很好的實現利器,至少我在inlineCallbacks這部分是真正的感受到了generator帶來的方便。

所以主要還是AIO的功勞,就像在Node.js裏面,實現類似的功能是比較方便的,因爲Node.js本身的IO都是AIO,所以只要修改語法樹,就是可以達到這種效果。


轉載:http://airekans.github.io/python/2012/07/17/inlinecallbacks/


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