到上一節(jié)結(jié)束,其實(shí)讀者已經(jīng)能夠做一個網(wǎng)站了,但是,僅僅用前面的技術(shù)來做的網(wǎng)站,僅能算一個小網(wǎng)站,在《為做網(wǎng)站而準(zhǔn)備》中,說明之所以選tornado,就是因?yàn)樗軌蚪鉀Qc10k問題,即能夠?qū)崿F(xiàn)大用戶量訪問。
要實(shí)現(xiàn)大用戶量訪問,必須要做的就是:異步。除非你是很土的土豪。
有不少資料對這兩個概念做了不同角度和層面的解釋。在我來看,一個最典型的例子就是打電話和發(fā)短信。
以上方式理解“同步”和“異步”不是很精準(zhǔn),有些地方或有牽強(qiáng)。要嚴(yán)格理解,需要用嚴(yán)格一點(diǎn)的定義表述(以下表述參照了知乎上的回答):
同步和異步關(guān)注的是消息通信機(jī)制 (synchronous communication/ asynchronous communication)
所謂同步,就是在發(fā)出一個“調(diào)用”時,在沒有得到結(jié)果之前,該“調(diào)用”就不返回。但是一旦調(diào)用返回,就得到返回值了。 換句話說,就是由“調(diào)用者”主動等待這個“調(diào)用”的結(jié)果。
而異步則是相反,“調(diào)用”在發(fā)出之后,這個調(diào)用就直接返回了,所以沒有返回結(jié)果。換句話說,當(dāng)一個異步過程調(diào)用發(fā)出后,調(diào)用者不會立刻得到結(jié)果。而是在“調(diào)用”發(fā)出后,“被調(diào)用者”通過狀態(tài)、通知來通知調(diào)用者,或通過回調(diào)函數(shù)處理這個調(diào)用。
可能還是前面的打電話和發(fā)短信更好理解。
“阻塞和非阻塞”與“同步和異步”常常被換為一談,其實(shí)它們之間還是有差別的。如果按照一個“差不多”先生的思維方法,你也可以不那么深究它們之間的學(xué)理上的差距,反正在你的程序中,會使用就可以了。不過,必要的嚴(yán)謹(jǐn)還是需要的,特別是我寫這個教程,要裝扮的讓別人看來自己懂,于是就再引用知乎上的說明(我個人認(rèn)為,別人已經(jīng)做的挺好的東西,就別重復(fù)勞動了,“拿來主義”,也不錯。或許你說我抄襲和山寨,但是我明確告訴你來源了):
阻塞和非阻塞關(guān)注的是程序在等待調(diào)用結(jié)果(消息,返回值)時的狀態(tài).
阻塞調(diào)用是指調(diào)用結(jié)果返回之前,當(dāng)前線程會被掛起。調(diào)用線程只有在得到結(jié)果之后才會返回。非阻塞調(diào)用指在不能立刻得到結(jié)果之前,該調(diào)用不會阻塞當(dāng)前線程。
按照這個說明,發(fā)短信就是顯然的非阻塞,發(fā)出去一條短信之后,你利用手機(jī)還可以干別的,乃至于再發(fā)一條“老齊的課程沒意思,還是看PHP刺激”也是可以的。
關(guān)于這兩組基本概念的辨析,不是本教程的重點(diǎn),讀者可以參閱這篇文章:http://www.cppblog.com/converse/archive/2009/05/13/82879.html,文章作者做了細(xì)致入微的辨析。
此前,在tornado基礎(chǔ)上已經(jīng)完成的web,就是同步的、阻塞的。為了更明顯的感受這點(diǎn),不妨這樣試一試。
在handlers文件夾中建立一個文件,命名為sleep.py
#!/usr/bin/env python
# coding=utf-8
from base import BaseHandler
import time
class SleepHandler(BaseHandler):
def get(self):
time.sleep(17)
self.render("sleep.html")
class SeeHandler(BaseHandler):
def get(self):
self.render("see.html")
其它的事情,如果讀者對我在《用tornado做網(wǎng)站(1)》中所講述的網(wǎng)站框架熟悉,應(yīng)該知道如何做了,不熟悉,請回頭復(fù)習(xí)。
sleep.html和see.html是兩個簡單的模板,內(nèi)容可以自己寫。別忘記修改url.py中的目錄。
然后的測試稍微復(fù)雜一點(diǎn)點(diǎn),就是打開瀏覽器之后,打開兩個標(biāo)簽,分別在兩個標(biāo)簽中輸入localhost:8000/sleep
(記為標(biāo)簽1)和localhost:8000/see
(記為標(biāo)簽2),注意我用的是8000端口。輸入之后先不要點(diǎn)擊回車去訪問。做好準(zhǔn)備,記住切換標(biāo)簽可以用“ctrl-tab”組合鍵。
建議讀者修改sleep.py中的time.sleep(17)這個值,多試試。很好玩的吧。
當(dāng)然,這是比較笨拙的方法,本來是可以通過測試工具完成上述操作比較的。怎奈要用別的工具,還要進(jìn)行介紹,又多了一個分散精力的東西,故用如此笨拙的方法,權(quán)當(dāng)有一個體會。
tornado本來就是一個異步的服務(wù)框架,體現(xiàn)在tornado的服務(wù)器和客戶端的網(wǎng)絡(luò)交互的異步上,起作用的是tornado.ioloop.IOLoop。但是如果的客戶端請求服務(wù)器之后,在執(zhí)行某個方法的時候,比如上面的代碼中執(zhí)行g(shù)et()方法的時候,遇到了time.sleep(17)
這個需要執(zhí)行時間比較長的操作,耗費(fèi)時間,就會使整個tornado服務(wù)器的性能受限了。
為了解決這個問題,tornado提供了一套異步機(jī)制,就是異步裝飾器@tornado.web.asynchronous
:
#!/usr/bin/env python
# coding=utf-8
import tornado.web
from base import BaseHandler
import time
class SleepHandler(BaseHandler):
@tornado.web.asynchronous
def get(self):
tornado.ioloop.IOLoop.instance().add_timeout(time.time() + 17, callback=self.on_response)
def on_response(self):
self.render("sleep.html")
self.finish()
將sleep.py的代碼如上述一樣改造,即在get()方法前面增加了裝飾器@tornado.web.asynchronous
,它的作用在于將tornado服務(wù)器本身默認(rèn)的設(shè)置_auto_fininsh
值修改為false。如果不用這個裝飾器,客戶端訪問服務(wù)器的get()方法并得到返回值之后,兩只之間的連接就斷開了,但是用了@tornado.web.asynchronous
之后,這個連接就不關(guān)閉,直到執(zhí)行了self.finish()
才關(guān)閉這個連接。
tornado.ioloop.IOLoop.instance().add_timeout()
也是一個實(shí)現(xiàn)異步的函數(shù),time.time()+17
是給前面函數(shù)提供一個參數(shù),這樣實(shí)現(xiàn)了相當(dāng)于time.sleep(17)
的功能,不過,還沒有完成,當(dāng)這個操作完成之后,就執(zhí)行回調(diào)函數(shù)on_response()
中的self.render("sleep.html")
,并關(guān)閉連接self.finish()
。
過程清楚了。所謂異步,就是要解決原來的time.sleep(17)
造成的服務(wù)器處理時間長,性能下降的問題。解決方法如上描述。
讀者看這個代碼,或許感覺有點(diǎn)不是很舒服。如果有這么一點(diǎn)感覺,是正常的。因?yàn)樗锩娉搜b飾器之外,用到了一個回調(diào)函數(shù),它讓代碼的邏輯不是平鋪下去,而是被分割為了兩段。第一段是tornado.ioloop.IOLoop.instance().add_timeout(time.time() + 17, callback=self.on_response)
,用callback=self.on_response
來使用回調(diào)函數(shù),并沒有如同改造之前直接self.render("sleep.html")
;第二段是回調(diào)函數(shù)on_response(self),要在這個函數(shù)里面執(zhí)行
self.render("sleep.html"),并且以
self.finish()`結(jié)尾以關(guān)閉連接。
這還是執(zhí)行簡單邏輯,如果復(fù)雜了,不斷地要進(jìn)行“回調(diào)”,無法讓邏輯順利延續(xù),那面會“眩暈”了。這種現(xiàn)象被業(yè)界成為“代碼邏輯拆分”,打破了原有邏輯的順序性。為了讓代碼邏輯不至于被拆分的七零八落,于是就出現(xiàn)了另外一種常用的方法:
#!/usr/bin/env python
# coding=utf-8
import tornado.web
import tornado.gen
from base import BaseHandler
import time
class SleepHandler(tornado.web.RequestHandler):
@tornado.gen.coroutine
def get(self):
yield tornado.gen.Task(tornado.ioloop.IOLoop.instance().add_timeout, time.time() + 17)
#yield tornado.gen.sleep(17)
self.render("sleep.html")
從整體上看,這段代碼避免了回調(diào)函數(shù),看著順利多了。
再看細(xì)節(jié)部分。
首先使用的是@tornado.gen.coroutine
裝飾器,所以要在前面有import tornado.gen
。跟這個裝飾器類似的是@tornado.gen.engine
裝飾器,兩者功能類似,有一點(diǎn)細(xì)微差別。請閱讀官方對此的解釋:
This decorator(指engine) is similar to coroutine, except it does not return a Future and the callback argument is not treated specially.
@tornado.gen.engine
是古時候用的,現(xiàn)在我們都使用@tornado.gen.corroutine
了,這個是在tornado 3.0以后開始。在網(wǎng)上查閱資料的時候,會遇到一些使用@tornado.gen.engine
的,但是在你使用或者借鑒代碼的時候,就勇敢地將其修改為@tornado.gen.coroutine
好了。有了這個裝飾器,就能夠控制下面的生成器的流程了。
然后就看到get()方法里面的yield了,這是一個生成器(參閱本教程《生成器》)。yield tornado.gen.Task(tornado.ioloop.IOLoop.instance().add_timeout, time.time() + 17)
的執(zhí)行過程,應(yīng)該先看括號里面,跟前面的一樣,是來替代time.sleep(17)
的,然后是tornado.gen.Task()
方法,其作用是“Adapts a callback-based asynchronous function for use in coroutines.”(由于怕翻譯后遺漏信息,引用原文)。返回后,最后使用yield得到了一個生成器,先把流程掛起,等完全完畢,再喚醒繼續(xù)執(zhí)行。要提醒讀者,生成器都是異步的。
其實(shí),上面啰嗦一對,可以用代碼中注釋了的一句話來代替yield tornado.gen.sleep(17)
,之所以擴(kuò)所,就是為了順便看到tornado.gen.Task()
方法,因?yàn)槿绻x者在看古老的代碼時候,會遇到。但是,后面你寫的時候,就不要那么啰嗦了,請用yield tornado.gen.sleep()
。
至此,基本上對tornado的異步設(shè)置有了概覽,不過,上面的程序在實(shí)際中沒有什么價值。在工程中,要讓tornado網(wǎng)站真正異步起來,還要做很多事情,不僅僅是如上面的設(shè)置,因?yàn)楹芏鄸|西,其實(shí)都不是異步的。
以下各項(xiàng)同步(阻塞)的,如果在tornado中按照之前的方式只用它們,就是把tornado的非阻塞、異步優(yōu)勢削減了。
除了以上,或許在編程實(shí)踐中還會遇到其他的同步、阻塞實(shí)踐。僅僅就上面幾項(xiàng),就是編程實(shí)踐中經(jīng)常會遇到的,怎么解決?
聰明的大牛程序員幫我們做了擴(kuò)展模塊,專門用來實(shí)現(xiàn)異步/非阻塞的。
tornado.gen.sleep()
或者tornado.ioloop.IOLoop.instance().add_timeout
,這在前面代碼已經(jīng)顯示了。其它的解決方法,只能看到問題具體說了,甚至沒有很好的解決方法。不過,這里有一個列表,列出了足夠多的庫,供使用者選擇:Async Client Libraries built on tornado.ioloop,同時這個頁面里面還有很多別的鏈接,都是很好的資源,建議讀者多看看。
教程到這里,讀者是不是要思考一個問題,既然對于mongodb有專門的motor庫來實(shí)現(xiàn)異步,前面對于tornado的異步,不管是哪個裝飾器,都感覺麻煩,有沒有專門的庫來實(shí)現(xiàn)這種異步呢?這不是異想天開,還真有。也應(yīng)該有,因?yàn)檫@才體現(xiàn)python的特點(diǎn)。比如greenlet-tornado,就是一個不錯的庫。讀者可以瀏覽官方網(wǎng)站深入了解(為什么對mysql那么不積極呢?按理說應(yīng)該出來好多支持mysql異步的庫才對)。
必須聲明,前面演示如何在tornado中設(shè)置異步的代碼,僅僅是演示理解設(shè)置方法。在工程實(shí)踐中,那個代碼的意義不到。為此,應(yīng)該有一個近似于實(shí)踐的代碼示例。是的,的確應(yīng)該有。當(dāng)我正要寫這樣的代碼時候,在網(wǎng)上發(fā)現(xiàn)一篇文章,這篇文章阻止了我寫,因?yàn)槲乙獙懙哪瞧恼碌淖髡咴缇蛯懞昧?,而且我認(rèn)為表述非常到位,示例也詳細(xì)。所以,我不得不放棄,轉(zhuǎn)而推薦給讀者這篇好文章:
更多建議: