鍍金池/ 教程/ Python/ 用 tornado 做網(wǎng)站 (7)
標(biāo)準(zhǔn)庫 (4)
如何成為 Python 高手
標(biāo)準(zhǔn)庫 (6)
標(biāo)準(zhǔn)庫 (3)
類(2)
Pandas 使用 (2)
xml
用 tornado 做網(wǎng)站 (5)
文件(1)
練習(xí)
列表(3)
從小工到專家
除法
錯誤和異常 (2)
函數(shù)(1)
用 tornado 做網(wǎng)站 (7)
為做網(wǎng)站而準(zhǔn)備
函數(shù)練習(xí)
標(biāo)準(zhǔn)庫 (8)
Pandas 使用 (1)
回顧 list 和 str
字典(1)
用 tornado 做網(wǎng)站 (3)
字符串(1)
函數(shù)(2)
寫一個簡單的程序
將數(shù)據(jù)存入文件
語句(5)
SQLite 數(shù)據(jù)庫
集成開發(fā)環(huán)境(IDE)
集合(1)
類(1)
用 tornado 做網(wǎng)站 (6)
用 tornado 做網(wǎng)站 (2)
自省
語句(4)
錯誤和異常 (1)
用 tornado 做網(wǎng)站 (4)
集合(2)
列表(1)
標(biāo)準(zhǔn)庫 (1)
生成器
mysql 數(shù)據(jù)庫 (1)
第三方庫
實(shí)戰(zhàn)
運(yùn)算符
類(3)
字典(2)
語句(1)
數(shù)和四則運(yùn)算
語句(2)
文件(2)
MySQL 數(shù)據(jù)庫 (2)
電子表格
迭代器
mongodb 數(shù)據(jù)庫 (1)
特殊方法 (2)
特殊方法 (1)
字符編碼
編寫模塊
用 tornado 做網(wǎng)站 (1)
標(biāo)準(zhǔn)庫 (5)
函數(shù)(4)
類(5)
字符串(2)
關(guān)于 Python 的故事
函數(shù)(3)
字符串(4)
處理股票數(shù)據(jù)
常用數(shù)學(xué)函數(shù)和運(yùn)算優(yōu)先級
字符串(3)
為計(jì)算做準(zhǔn)備
多態(tài)和封裝
類(4)
迭代
語句(3)
錯誤和異常 (3)
分析 Hello
Python 安裝
標(biāo)準(zhǔn)庫 (2)
列表(2)
元組

用 tornado 做網(wǎng)站 (7)

到上一節(jié)結(jié)束,其實(shí)讀者已經(jīng)能夠做一個網(wǎng)站了,但是,僅僅用前面的技術(shù)來做的網(wǎng)站,僅能算一個小網(wǎng)站,在《為做網(wǎng)站而準(zhǔn)備》中,說明之所以選 tornado,就是因?yàn)樗軌蚪鉀Q c10k 問題,即能夠?qū)崿F(xiàn)大用戶量訪問。

要實(shí)現(xiàn)大用戶量訪問,必須要做的就是:異步。除非你是很土的土豪。

相關(guān)概念

同步和異步

有不少資料對這兩個概念做了不同角度和層面的解釋。在我來看,一個最典型的例子就是打電話和發(fā)短信。

  • 打電話就是同步。張三給李四打電話,張三說:“是李四嗎?”。當(dāng)這個信息被張三發(fā)出,提交給李四,就等待李四的響應(yīng)(一般會聽到“是”,或者“不是”),只有得到了李四返回的信息之后,才能進(jìn)行后續(xù)的信息傳送。
  • 發(fā)短信是異步。張三給李四發(fā)短信,編輯了一句話“今晚一起看老齊的零基礎(chǔ)學(xué) Python”,發(fā)送給李四。李四或許馬上回復(fù),或許過一段時間,這段時間多長也不定,才回復(fù)。總之,李四不管什么時候回復(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ù)勞動了,“拿來主義”,也不錯?;蛟S你說我抄襲和山寨,但是我明確告訴你來源了):

阻塞和非阻塞關(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 的同步

此前,在 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”組合鍵。

  1. 執(zhí)行標(biāo)簽 1,讓它訪問網(wǎng)站;
  2. 馬上切換到標(biāo)簽 2,訪問網(wǎng)址。
  3. 注意觀察,兩個標(biāo)簽頁面,是不是都在顯示正在訪問,請等待。
  4. 當(dāng)標(biāo)簽 1 不呈現(xiàn)等待提示(比如一個正在轉(zhuǎn)的圓圈)時,標(biāo)簽 2 的表現(xiàn)如何?幾乎同時也訪問成功了。

建議讀者修改 sleep.py 中的 time.sleep(17) 這個值,多試試。很好玩的吧。

當(dāng)然,這是比較笨拙的方法,本來是可以通過測試工具完成上述操作比較的。怎奈要用別的工具,還要進(jìn)行介紹,又多了一個分散精力的東西,故用如此笨拙的方法,權(quán)當(dāng)有一個體會。

異步設(shè)置

tornado 本來就是一個異步的服務(wù)框架,體現(xiàn)在 tornado 的服務(wù)器和客戶端的網(wǎng)絡(luò)交互的異步上,起作用的是 tornado.ioloop.IOLoop。但是如果的客戶端請求服務(wù)器之后,在執(zhí)行某個方法的時候,比如上面的代碼中執(zhí)行 get() 方法的時候,遇到了 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í)都不是異步的。

實(shí)踐中的異步

以下各項(xiàng)同步(阻塞)的,如果在 tornado 中按照之前的方式只用它們,就是把 tornado 的非阻塞、異步優(yōu)勢削減了。

  • 數(shù)據(jù)庫的所有操作,不管你的數(shù)據(jù)是 SQL 還是 noSQL,connect、insert、update 等
  • 文件操作,打開,讀取,寫入等
  • time.sleep,在前面舉例中已經(jīng)看到了
  • smtplib,發(fā)郵件的操作
  • 一些網(wǎng)絡(luò)操作,比如 tornado 的 httpclient 以及 pycurl 等

除了以上,或許在編程實(shí)踐中還會遇到其他的同步、阻塞實(shí)踐。僅僅就上面幾項(xiàng),就是編程實(shí)踐中經(jīng)常會遇到的,怎么解決?

聰明的大牛程序員幫我們做了擴(kuò)展模塊,專門用來實(shí)現(xiàn)異步/非阻塞的。

  • 在數(shù)據(jù)庫方面,由于種類繁多,不能一一說明,比如 mysql,可以使用adb模塊來實(shí)現(xiàn) python 的異步 mysql 庫;對于 mongodb 數(shù)據(jù)庫,有一個非常優(yōu)秀的模塊,專門用于在 tornado 和 mongodb 上實(shí)現(xiàn)異步操作,它就是 motor。特別貼出它的 logo,我喜歡。官方網(wǎng)站:http://motor.readthedocs.org/en/stable/上的安裝和使用方法都很詳細(xì)。

http://wiki.jikexueyuan.com/project/start-learning-python/images/30901.png" alt="" />

  • 文件操作方面也沒有替代模塊,只能盡量控制好 IO,或者使用內(nèi)存型(Redis)及文檔型(MongoDB)數(shù)據(jù)庫。
  • time.sleep() 在 tornado 中有替代:tornado.gen.sleep() 或者 tornado.ioloop.IOLoop.instance().add_timeout,這在前面代碼已經(jīng)顯示了。
  • smtp 發(fā)送郵件,推薦改為 tornado-smtp-client。
  • 對于網(wǎng)絡(luò)操作,要使用 tornado.httpclient.AsyncHTTPClient。

其它的解決方法,只能看到問題具體說了,甚至沒有很好的解決方法。不過,這里有一個列表,列出了足夠多的庫,供使用者選擇: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)槲乙獙懙哪瞧恼碌淖髡咴缇蛯懞昧耍椅艺J(rèn)為表述非常到位,示例也詳細(xì)。所以,我不得不放棄,轉(zhuǎn)而推薦給讀者這篇好文章:

舉例:http://emptysqua.re/blog/refactoring-tornado-coroutines/


總目錄   |   上節(jié):用 tornado 做網(wǎng)站 (6)   |   下節(jié):為計(jì)算做準(zhǔn)備

如果你認(rèn)為有必要打賞我,請通過支付寶:qiwsir@126.com,不勝感激。

上一篇:電子表格下一篇:字典(2)