Monday, April 30, 2007

Generator與串接Asynchronous calls

我們時常必須以一定的順序呼叫底層module所提供的函式。比如說,
def callSeq(api):
    api.setEnv()
    api.produce()
    api.report()
這在任何procedural language當中都是很直覺的事情。但是GUI app的開發原則是,UI thread必須能夠很快的消耗event queue,並且把繁重的工作交給其他thread執行。因此,我們時常會期望API所提供的內容是asynchronous的,如此一來GUI的反應才會順暢。如此一來,我們就必須「等待」每一個async. call的結果,才能夠進行下一個動作。想像上,我們可以這麼做:
def createBatch():
    jobs = workerthread.todoQueue
    #jobs is a thread-safe queue
    jobs.push(api.setEnv)
    jobs.push(api.produce)
    jobs.push(api.report)
接著,設計workerthread,讓他從todoQueue當中不斷找事情做。缺點是,我們仍然必須要在workerthread當中處理api的各種callback,或是event,才能在一個task結束之後,繼續下一個task。同時,我們也必須對queue進行各種操作與管理。這時候generator便可以幫上忙。
def thingsTodo():
    yield api.setEnv()
    yield api.produce()
    yield api.report()

todo = thingsTodo()

#here we assume that the "task completion"
#is passed to an unified callback
def asyncCallback(event):
    try:
        if(event == 'done'):
            todo.next()
    except StopIteration:
        print("all jobs done")

api.setAsyncCallback(asyncCallback)

#then we invoke the first thing to do
todo.next()
我們相當於是利用了generator來幫我們進行簿記的動作。值得注意的是,todo.next()必須在逸出current scope之後仍然有效。這也就是closure的觀念…在local function之中,使用了參數、local變數,以及他們的衍生結果,而這個local function可以被外部的object所使用,並且在local scope結束之後,仍然有效。 這樣的作法,很簡單就可以定義出calling sequence。事實上,我們可以把thingsTodo()真正要做的,想成「在這個function的這裡先暫停一下,之後再回來繼續」的概念(yield時return,但是記住狀態。下次的function entry point移從yield敘述句之後)。這樣的概念叫做稱為continuation。我們只是用generator來模擬這樣的效果。由於Python的generator有著不少限制,所以目前並不算是直接在語言層級支援continuation的語言。 題外話,如果api提供的介面是像這樣:
api.setEnv( callbackWhenDone )
api.produce( callbackWhenDone )
api.report( callbackWhenDone )
那麼我們可以這樣把呼叫串連起來
def callDone(event):
    print "done"
def callReport(event):
    api.report(callDone)
def callProduce(event):
    api.produce(callReport)
api.setEnv( callProduce )
當然,callback一般都需要判斷回傳event為成功或是失敗,所以實際狀況會更為繁瑣。無論如何,closure與local function,這兩種可以動態決定function definition的功能,讓我們免於使用額外的資料結構處理關於api calling sequence的維護,可以讓彼此有關係的code segment距離接近,進而提升程式的可讀性。

No comments: