Tornado框架09-異步01

01-同步

我們用兩個函數來模擬兩個客戶端請求,並依次進行處理:

# coding:utf-8

def req_a():
    """模擬請求a"""
    print '開始處理請求req_a'
    print '完成處理請求req_a'

def req_b():
    """模擬請求b"""
    print '開始處理請求req_b'
    print '完成處理請求req_b'

def main():
    """模擬tornado框架,處理兩個請求"""
    req_a()
    req_b()

if __name__ == "__main__":
    main()

執行結果:

開始處理請求req_a
完成處理請求req_a
開始處理請求req_b
完成處理請求req_b

同步是按部就班的依次執行,始終按照同一個步調執行,上一個步驟未執行完不會執行下一步。

想一想,如果在處理請求req_a時需要執行一個耗時的工作(如IO),其執行過程如何?

# coding:utf-8

import time

def long_io():
    """模擬耗時IO操作"""
    print "開始執行IO操作"
    time.sleep(5)
    print "完成IO操作"
    return "io result"

def req_a():
    print "開始處理請求req_a"
    ret = long_io()
    print "ret: %s" % ret
    print "完成處理請求req_a"

def req_b():
    print "開始處理請求req_b"
    print "完成處理請求req_b"

def main():
    req_a()
    req_b()

if __name__=="__main__":
    main()

執行過程:

開始處理請求req_a
開始執行IO操作
完成IO操作
完成處理請求req_a
開始處理請求req_b
完成處理請求req_b

在上面的測試中,我們看到耗時的操作會將代碼執行阻塞住,即req_a未處理完req_b是無法執行的。
我們怎麼解決耗時操作阻塞代碼執行?

02-異步

對於耗時的過程,我們將其交給別人(如其另外一個線程)去執行,而我們繼續往下處理,當別人執行完耗時操作後再將結果反饋給我們,這就是我們所說的異步。
我們用容易理解的線程機制來實現異步。

回調寫法實現原理

# coding:utf-8

import time
import thread

def long_io(callback):
    """將耗時的操作交給另一線程來處理"""
    def fun(cb): # 回調函數作爲參數
        """耗時操作"""
        print "開始執行IO操作"
        time.sleep(5)
        print "完成IO操作,並執行回調函數"
        cb("io result")  # 執行回調函數
    thread.start_new_thread(fun, (callback,))  # 開啓線程執行耗時操作

def on_finish(ret):
    """回調函數"""
    print "開始執行回調函數on_finish"
    print "ret: %s" % ret
    print "完成執行回調函數on_finish"

def req_a():
    print "開始處理請求req_a" 
    long_io(on_finish)
    print "離開處理請求req_a"

def req_b():
    print "開始處理請求req_b"
    time.sleep(2) # 添加此句來突出顯示程序執行的過程
    print "完成處理請求req_b"

def main():
    req_a()
    req_b()
    while 1: # 添加此句防止程序退出,保證線程可以執行完
        pass

if __name__ == '__main__':
    main()

執行過程:

開始處理請求req_a
離開處理請求req_a
開始處理請求req_b
開始執行IO操作
完成處理請求req_b
完成IO操作,並執行回調函數
開始執行回調函數on_finish
ret: io result
完成執行回調函數on_finish

異步的特點是程序存在多個步調,即本屬於同一個過程的代碼可能在不同的步調上同時執行。

協程寫法實現原理

在使用回調函數寫異步程序時,需將本屬於一個執行邏輯(處理請求a)的代碼拆分成兩個函數req_a和on_finish,這與同步程序的寫法相差很大。而同步程序更便於理解業務邏輯,所以我們能否用同步代碼的寫法來編寫異步程序?回想yield關鍵字的作用?

初始版本

# coding:utf-8

import time
import thread

gen = None # 全局生成器,供long_io使用

def long_io():
    def fun():
        print "開始執行IO操作"
        global gen
        time.sleep(5)
        try:
            print "完成IO操作,並send結果喚醒掛起程序繼續執行"
            gen.send("io result")  # 使用send返回結果並喚醒程序繼續執行
        except StopIteration: # 捕獲生成器完成迭代,防止程序退出
            pass
    thread.start_new_thread(fun, ())

def req_a():
    print "開始處理請求req_a"
    ret = yield long_io()
    print "ret: %s" % ret
    print "完成處理請求req_a"

def req_b():
    print "開始處理請求req_b"
    time.sleep(2)
    print "完成處理請求req_b"

def main():
    global gen
    gen = req_a()
    gen.next() # 開啓生成器req_a的執行
    req_b()
    while 1:
        pass

if __name__ == '__main__':
    main()

執行過程:

開始處理請求req_a
開始處理請求req_b
開始執行IO操作
完成處理請求req_b
完成IO操作,並send結果喚醒掛起程序繼續執行
ret: io result
完成處理請求req_a

升級版本
我們在上面編寫出的版本雖然req_a的編寫方式很類似與同步代碼,但是在main中調用req_a的時候卻不能將其簡單的視爲普通函數,而是需要作爲生成器對待。

現在,我們試圖嘗試修改,讓req_a與main的編寫都類似與同步代碼。

# coding:utf-8

import time
import thread

gen = None # 全局生成器,供long_io使用

def gen_coroutine(f):
    def wrapper(*args, **kwargs):
        global gen
        gen = f()
        gen.next()
    return wrapper

def long_io():
    def fun():
        print "開始執行IO操作"
        global gen
        time.sleep(5)
        try:
            print "完成IO操作,並send結果喚醒掛起程序繼續執行"
            gen.send("io result")  # 使用send返回結果並喚醒程序繼續執行
        except StopIteration: # 捕獲生成器完成迭代,防止程序退出
            pass
    thread.start_new_thread(fun, ())

@gen_coroutine
def req_a():
    print "開始處理請求req_a"
    ret = yield long_io()
    print "ret: %s" % ret
    print "完成處理請求req_a"

def req_b():
    print "開始處理請求req_b"
    time.sleep(2)
    print "完成處理請求req_b"

def main():
    req_a()
    req_b()
    while 1:
        pass

if __name__ == '__main__':
    main()

執行過程:

開始處理請求req_a
開始處理請求req_b
開始執行IO操作
完成處理請求req_b
完成IO操作,並send結果喚醒掛起程序繼續執行
ret: io result
完成處理請求req_a

最終版本
剛剛完成的版本依然不理想,因爲存在一個全局變量gen來供long_io使用。我們現在再次改寫程序,消除全局變量gen。

# coding:utf-8

import time
import thread

def gen_coroutine(f):
    def wrapper(*args, **kwargs):
        gen_f = f()  # gen_f爲生成器req_a
        r = gen_f.next()  # r爲生成器long_io
        def fun(g):
            ret = g.next() # 執行生成器long_io
            try:
                gen_f.send(ret) # 將結果返回給req_a並使其繼續執行
            except StopIteration:
                pass
        thread.start_new_thread(fun, (r,))
    return wrapper

def long_io():
    print "開始執行IO操作"
    time.sleep(5)
    print "完成IO操作,yield回操作結果"
    yield "io result"

@gen_coroutine
def req_a():
    print "開始處理請求req_a"
    ret = yield long_io()
    print "ret: %s" % ret
    print "完成處理請求req_a"

def req_b():
    print "開始處理請求req_b"
    time.sleep(2)
    print "完成處理請求req_b"

def main():
    req_a()
    req_b()
    while 1:
        pass

if __name__ == '__main__':
    main()

執行過程:

開始處理請求req_a
開始處理請求req_b
開始執行IO操作
完成處理請求req_b
完成IO操作,yield回操作結果
ret: io result
完成處理請求req_a

這個最終版本就是理解Tornado異步編程原理的最簡易模型,但是,Tornado實現異步的機制不是線程,而是epoll,即將異步過程交給epoll執行並進行監視回調。

需要注意的一點是,我們實現的版本嚴格意義上來說不能算是協程,因爲兩個程序的掛起與喚醒是在兩個線程上實現的,而Tornado利用epoll來實現異步,程序的掛起與喚醒始終在一個線程上,由Tornado自己來調度,屬於真正意義上的協程。雖如此,並不妨礙我們理解Tornado異步編程的原理。

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