認識異步
1. 同步
我們用兩個函數來模擬兩個客戶端請求,並依次進行處理:
# 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是無法執行的。
我們怎麼解決耗時操作阻塞代碼執行?
2. 異步
對於耗時的過程,我們將其交給別人(如其另外一個線程)去執行,而我們繼續往下處理,當別人執行完耗時操作後再將結果反饋給我們,這就是我們所說的異步。
我們用容易理解的線程機制來實現異步。
2.1 回調寫法實現原理
# 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
異步的特點是程序存在多個步調,即本屬於同一個過程的代碼可能在不同的步調上同時執行。
2.2 協程寫法實現原理
在使用回調函數寫異步程序時,需將本屬於一個執行邏輯(處理請求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異步編程的原理。
思考
- Tornado裏的異步就是協程,這句話對嗎?
- Tornado中出現yield就是異步,這句話對嗎?
- 怎麼理解yield將程序掛起?在Tornado中又如何理解yield掛起程序實現異步?