7.1 認(rèn)識異步
1. 同步
我們用兩個函數(shù)來模擬兩個客戶端請求,并依次進行處理:
# 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()
執(zhí)行結(jié)果:
開始處理請求req_a
完成處理請求req_a
開始處理請求req_b
完成處理請求req_b
同步是按部就班的依次執(zhí)行,始終按照同一個步調(diào)執(zhí)行,上一個步驟未執(zhí)行完不會執(zhí)行下一步。
想一想,如果在處理請求req_a時需要執(zhí)行一個耗時的工作(如IO),其執(zhí)行過程如何?
# coding:utf-8
import time
def long_io():
"""模擬耗時IO操作"""
print "開始執(zhí)行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()
執(zhí)行過程:
開始處理請求req_a
開始執(zhí)行IO操作
完成IO操作
完成處理請求req_a
開始處理請求req_b
完成處理請求req_b
在上面的測試中,我們看到耗時的操作會將代碼執(zhí)行阻塞住,即req_a未處理完req_b是無法執(zhí)行的。
我們怎么解決耗時操作阻塞代碼執(zhí)行?
2. 異步
對于耗時的過程,我們將其交給別人(如其另外一個線程)去執(zhí)行,而我們繼續(xù)往下處理,當(dāng)別人執(zhí)行完耗時操作后再將結(jié)果反饋給我們,這就是我們所說的異步。
我們用容易理解的線程機制來實現(xiàn)異步。
2.1 回調(diào)寫法實現(xiàn)原理
# coding:utf-8
import time
import thread
def long_io(callback):
"""將耗時的操作交給另一線程來處理"""
def fun(cb): # 回調(diào)函數(shù)作為參數(shù)
"""耗時操作"""
print "開始執(zhí)行IO操作"
time.sleep(5)
print "完成IO操作,并執(zhí)行回調(diào)函數(shù)"
cb("io result") # 執(zhí)行回調(diào)函數(shù)
thread.start_new_thread(fun, (callback,)) # 開啟線程執(zhí)行耗時操作
def on_finish(ret):
"""回調(diào)函數(shù)"""
print "開始執(zhí)行回調(diào)函數(shù)on_finish"
print "ret: %s" % ret
print "完成執(zhí)行回調(diào)函數(shù)on_finish"
def req_a():
print "開始處理請求req_a"
long_io(on_finish)
print "離開處理請求req_a"
def req_b():
print "開始處理請求req_b"
time.sleep(2) # 添加此句來突出顯示程序執(zhí)行的過程
print "完成處理請求req_b"
def main():
req_a()
req_b()
while 1: # 添加此句防止程序退出,保證線程可以執(zhí)行完
pass
if __name__ == '__main__':
main()
執(zhí)行過程:
開始處理請求req_a
離開處理請求req_a
開始處理請求req_b
開始執(zhí)行IO操作
完成處理請求req_b
完成IO操作,并執(zhí)行回調(diào)函數(shù)
開始執(zhí)行回調(diào)函數(shù)on_finish
ret: io result
完成執(zhí)行回調(diào)函數(shù)on_finish
異步的特點是程序存在多個步調(diào),即本屬于同一個過程的代碼可能在不同的步調(diào)上同時執(zhí)行。
2.2 協(xié)程寫法實現(xiàn)原理
在使用回調(diào)函數(shù)寫異步程序時,需將本屬于一個執(zhí)行邏輯(處理請求a)的代碼拆分成兩個函數(shù)req_a和on_finish,這與同步程序的寫法相差很大。而同步程序更便于理解業(yè)務(wù)邏輯,所以我們能否用同步代碼的寫法來編寫異步程序?
回想yield關(guān)鍵字的作用?
初始版本
# coding:utf-8
import time
import thread
gen = None # 全局生成器,供long_io使用
def long_io():
def fun():
print "開始執(zhí)行IO操作"
global gen
time.sleep(5)
try:
print "完成IO操作,并send結(jié)果喚醒掛起程序繼續(xù)執(zhí)行"
gen.send("io result") # 使用send返回結(jié)果并喚醒程序繼續(xù)執(zhí)行
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的執(zhí)行
req_b()
while 1:
pass
if __name__ == '__main__':
main()
執(zhí)行過程:
開始處理請求req_a
開始處理請求req_b
開始執(zhí)行IO操作
完成處理請求req_b
完成IO操作,并send結(jié)果喚醒掛起程序繼續(xù)執(zhí)行
ret: io result
完成處理請求req_a
升級版本
我們在上面編寫出的版本雖然req_a的編寫方式很類似與同步代碼,但是在main中調(diào)用req_a的時候卻不能將其簡單的視為普通函數(shù),而是需要作為生成器對待。
現(xiàn)在,我們試圖嘗試修改,讓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 "開始執(zhí)行IO操作"
global gen
time.sleep(5)
try:
print "完成IO操作,并send結(jié)果喚醒掛起程序繼續(xù)執(zhí)行"
gen.send("io result") # 使用send返回結(jié)果并喚醒程序繼續(xù)執(zhí)行
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()
執(zhí)行過程:
開始處理請求req_a
開始處理請求req_b
開始執(zhí)行IO操作
完成處理請求req_b
完成IO操作,并send結(jié)果喚醒掛起程序繼續(xù)執(zhí)行
ret: io result
完成處理請求req_a
最終版本
剛剛完成的版本依然不理想,因為存在一個全局變量gen來供long_io使用。我們現(xiàn)在再次改寫程序,消除全局變量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() # 執(zhí)行生成器long_io
try:
gen_f.send(ret) # 將結(jié)果返回給req_a并使其繼續(xù)執(zhí)行
except StopIteration:
pass
thread.start_new_thread(fun, (r,))
return wrapper
def long_io():
print "開始執(zhí)行IO操作"
time.sleep(5)
print "完成IO操作,yield回操作結(jié)果"
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()
執(zhí)行過程:
開始處理請求req_a
開始處理請求req_b
開始執(zhí)行IO操作
完成處理請求req_b
完成IO操作,yield回操作結(jié)果
ret: io result
完成處理請求req_a
這個最終版本就是理解Tornado異步編程原理的最簡易模型,但是,Tornado實現(xiàn)異步的機制不是線程,而是epoll,即將異步過程交給epoll執(zhí)行并進行監(jiān)視回調(diào)。
需要注意的一點是,我們實現(xiàn)的版本嚴(yán)格意義上來說不能算是協(xié)程,因為兩個程序的掛起與喚醒是在兩個線程上實現(xiàn)的,而Tornado利用epoll來實現(xiàn)異步,程序的掛起與喚醒始終在一個線程上,由Tornado自己來調(diào)度,屬于真正意義上的協(xié)程。雖如此,并不妨礙我們理解Tornado異步編程的原理。
思考
Tornado里的異步就是協(xié)程,這句話對嗎?
Tornado中出現(xiàn)yield就是異步,這句話對嗎?
怎么理解yield將程序掛起?在Tornado中又如何理解yield掛起程序?qū)崿F(xiàn)異步?