到上一節結束,其實讀者已經能夠做一個網站了,但是,僅僅用前面的技術來做的網站,僅能算一個小網站,在[《為做網站而準備》](https://github.com/qiwsir/StarterLearningPython/blob/master/301.md)中,說明之所以選tornado,就是因為它能夠解決c10k問題,即能夠實現大用戶量訪問。
要實現大用戶量訪問,必須要做的就是:異步。除非你是很土的土豪。
## [](https://github.com/qiwsir/StarterLearningPython/blob/master/309.md#相關概念)相關概念
### [](https://github.com/qiwsir/StarterLearningPython/blob/master/309.md#同步和異步)同步和異步
有不少資料對這兩個概念做了不同角度和層面的解釋。在我來看,一個最典型的例子就是打電話和發短信。
* 打電話就是同步。張三給李四打電話,張三說:“是李四嗎?”。當這個信息被張三發出,提交給李四,就等待李四的響應(一般會聽到“是”,或者“不是”),只有得到了李四返回的信息之后,才能進行后續的信息傳送。
* 發短信是異步。張三給李四發短信,編輯了一句話“今晚一起看老齊的零基礎學python”,發送給李四。李四或許馬上回復,或許過一段時間,這段時間多長也不定,才回復。總之,李四不管什么時候回復,張三會以聽到短信鈴聲為提示查看短信。
以上方式理解“同步”和“異步”不是很精準,有些地方或有牽強。要嚴格理解,需要用嚴格一點的定義表述(以下表述參照了[知乎](http://www.zhihu.com/question/19732473)上的回答):
> 同步和異步關注的是消息通信機制 (synchronous communication/ asynchronous communication)
>
> 所謂同步,就是在發出一個“調用”時,在沒有得到結果之前,該“調用”就不返回。但是一旦調用返回,就得到返回值了。 換句話說,就是由“調用者”主動等待這個“調用”的結果。
>
> 而異步則是相反,“調用”在發出之后,這個調用就直接返回了,所以沒有返回結果。換句話說,當一個異步過程調用發出后,調用者不會立刻得到結果。而是在“調用”發出后,“被調用者”通過狀態、通知來通知調用者,或通過回調函數處理這個調用。
可能還是前面的打電話和發短信更好理解。
### [](https://github.com/qiwsir/StarterLearningPython/blob/master/309.md#阻塞和非阻塞)阻塞和非阻塞
“阻塞和非阻塞”與“同步和異步”常常被換為一談,其實它們之間還是有差別的。如果按照一個“差不多”先生的思維方法,你也可以不那么深究它們之間的學理上的差距,反正在你的程序中,會使用就可以了。不過,必要的嚴謹還是需要的,特別是我寫這個教程,要裝扮的讓別人看來自己懂,于是就再引用[知乎](http://www.zhihu.com/question/19732473)上的說明(我個人認為,別人已經做的挺好的東西,就別重復勞動了,“拿來主義”,也不錯。或許你說我抄襲和山寨,但是我明確告訴你來源了):
> 阻塞和非阻塞關注的是程序在等待調用結果(消息,返回值)時的狀態.
>
> 阻塞調用是指調用結果返回之前,當前線程會被掛起。調用線程只有在得到結果之后才會返回。非阻塞調用指在不能立刻得到結果之前,該調用不會阻塞當前線程。
按照這個說明,發短信就是顯然的非阻塞,發出去一條短信之后,你利用手機還可以干別的,乃至于再發一條“老齊的課程沒意思,還是看PHP刺激”也是可以的。
關于這兩組基本概念的辨析,不是本教程的重點,讀者可以參閱這篇文章:[http://www.cppblog.com/converse/archive/2009/05/13/82879.html](http://www.cppblog.com/converse/archive/2009/05/13/82879.html),文章作者做了細致入微的辨析。
## [](https://github.com/qiwsir/StarterLearningPython/blob/master/309.md#tornado的同步)tornado的同步
此前,在tornado基礎上已經完成的web,就是同步的、阻塞的。為了更明顯的感受這點,不妨這樣試一試。
在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做網站(1)》](https://github.com/qiwsir/StarterLearningPython/blob/master/303.md)中所講述的網站框架熟悉,應該知道如何做了,不熟悉,請回頭復習。
sleep.html和see.html是兩個簡單的模板,內容可以自己寫。別忘記修改url.py中的目錄。
然后的測試稍微復雜一點點,就是打開瀏覽器之后,打開兩個標簽,分別在兩個標簽中輸入`localhost:8000/sleep`(記為標簽1)和`localhost:8000/see`(記為標簽2),注意我用的是8000端口。輸入之后先不要點擊回車去訪問。做好準備,記住切換標簽可以用“ctrl-tab”組合鍵。
1. 執行標簽1,讓它訪問網站;
2. 馬上切換到標簽2,訪問網址。
3. 注意觀察,兩個標簽頁面,是不是都在顯示正在訪問,請等待。
4. 當標簽1不呈現等待提示(比如一個正在轉的圓圈)時,標簽2的表現如何?幾乎同時也訪問成功了。
建議讀者修改sleep.py中的time.sleep(17)這個值,多試試。很好玩的吧。
當然,這是比較笨拙的方法,本來是可以通過測試工具完成上述操作比較的。怎奈要用別的工具,還要進行介紹,又多了一個分散精力的東西,故用如此笨拙的方法,權當有一個體會。
## [](https://github.com/qiwsir/StarterLearningPython/blob/master/309.md#異步設置)異步設置
tornado本來就是一個異步的服務框架,體現在tornado的服務器和客戶端的網絡交互的異步上,起作用的是tornado.ioloop.IOLoop。但是如果的客戶端請求服務器之后,在執行某個方法的時候,比如上面的代碼中執行get()方法的時候,遇到了`time.sleep(17)`這個需要執行時間比較長的操作,耗費時間,就會使整個tornado服務器的性能受限了。
為了解決這個問題,tornado提供了一套異步機制,就是異步裝飾器`@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服務器本身默認的設置`_auto_fininsh`值修改為false。如果不用這個裝飾器,客戶端訪問服務器的get()方法并得到返回值之后,兩只之間的連接就斷開了,但是用了`@tornado.web.asynchronous`之后,這個連接就不關閉,直到執行了`self.finish()`才關閉這個連接。
`tornado.ioloop.IOLoop.instance().add_timeout()`也是一個實現異步的函數,`time.time()+17`是給前面函數提供一個參數,這樣實現了相當于`time.sleep(17)`的功能,不過,還沒有完成,當這個操作完成之后,就執行回調函數`on_response()`中的`self.render("sleep.html")`,并關閉連接`self.finish()`。
過程清楚了。所謂異步,就是要解決原來的`time.sleep(17)`造成的服務器處理時間長,性能下降的問題。解決方法如上描述。
讀者看這個代碼,或許感覺有點不是很舒服。如果有這么一點感覺,是正常的。因為它里面除了裝飾器之外,用到了一個回調函數,它讓代碼的邏輯不是平鋪下去,而是被分割為了兩段。第一段是`tornado.ioloop.IOLoop.instance().add_timeout(time.time() + 17, callback=self.on_response)`,用`callback=self.on_response`來使用回調函數,并沒有如同改造之前直接`self.render("sleep.html")`;第二段是回調函數on_response(self)`,要在這個函數里面執行`self.render("sleep.html")`,并且以`self.finish()`結尾以關閉連接。
這還是執行簡單邏輯,如果復雜了,不斷地要進行“回調”,無法讓邏輯順利延續,那面會“眩暈”了。這種現象被業界成為“代碼邏輯拆分”,打破了原有邏輯的順序性。為了讓代碼邏輯不至于被拆分的七零八落,于是就出現了另外一種常用的方法:
~~~
#!/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")
~~~
從整體上看,這段代碼避免了回調函數,看著順利多了。
再看細節部分。
首先使用的是`@tornado.gen.coroutine`裝飾器,所以要在前面有`import tornado.gen`。跟這個裝飾器類似的是`@tornado.gen.engine`裝飾器,兩者功能類似,有一點細微差別。請閱讀[官方對此的解釋](http://www.tornadoweb.org/en/stable/gen.html):
> 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`是古時候用的,現在我們都使用`@tornado.gen.corroutine`了,這個是在tornado 3.0以后開始。在網上查閱資料的時候,會遇到一些使用`@tornado.gen.engine`的,但是在你使用或者借鑒代碼的時候,就勇敢地將其修改為`@tornado.gen.coroutine`好了。有了這個裝飾器,就能夠控制下面的生成器的流程了。
然后就看到get()方法里面的yield了,這是一個生成器(參閱本教程[《生成器》](https://github.com/qiwsir/StarterLearningPython/blob/master/215.md))。`yield tornado.gen.Task(tornado.ioloop.IOLoop.instance().add_timeout, time.time() + 17)`的執行過程,應該先看括號里面,跟前面的一樣,是來替代`time.sleep(17)`的,然后是`tornado.gen.Task()`方法,其作用是“Adapts a callback-based asynchronous function for use in coroutines.”(由于怕翻譯后遺漏信息,引用[原文](http://tornado.readthedocs.org/en/latest/gen.html))。返回后,最后使用yield得到了一個生成器,先把流程掛起,等完全完畢,再喚醒繼續執行。要提醒讀者,生成器都是異步的。
其實,上面啰嗦一對,可以用代碼中注釋了的一句話來代替`yield tornado.gen.sleep(17)`,之所以擴所,就是為了順便看到`tornado.gen.Task()`方法,因為如果讀者在看古老的代碼時候,會遇到。但是,后面你寫的時候,就不要那么啰嗦了,請用`yield tornado.gen.sleep()`。
至此,基本上對tornado的異步設置有了概覽,不過,上面的程序在實際中沒有什么價值。在工程中,要讓tornado網站真正異步起來,還要做很多事情,不僅僅是如上面的設置,因為很多東西,其實都不是異步的。
## [](https://github.com/qiwsir/StarterLearningPython/blob/master/309.md#實踐中的異步)實踐中的異步
以下各項同步(阻塞)的,如果在tornado中按照之前的方式只用它們,就是把tornado的非阻塞、異步優勢削減了。
* 數據庫的所有操作,不管你的數據是SQL還是noSQL,connect、insert、update等
* 文件操作,打開,讀取,寫入等
* time.sleep,在前面舉例中已經看到了
* smtplib,發郵件的操作
* 一些網絡操作,比如tornado的httpclient以及pycurl等
除了以上,或許在編程實踐中還會遇到其他的同步、阻塞實踐。僅僅就上面幾項,就是編程實踐中經常會遇到的,怎么解決?
聰明的大牛程序員幫我們做了擴展模塊,專門用來實現異步/非阻塞的。
* 在數據庫方面,由于種類繁多,不能一一說明,比如mysql,可以使用[adb](https://github.com/ovidiucp/pymysql-benchmarks)模塊來實現python的異步mysql庫;對于mongodb數據庫,有一個非常優秀的模塊,專門用于在tornado和mongodb上實現異步操作,它就是motor。特別貼出它的logo,我喜歡。官方網站:[http://motor.readthedocs.org/en/stable/](http://motor.readthedocs.org/en/stable/)上的安裝和使用方法都很詳細。
[](https://github.com/qiwsir/StarterLearningPython/blob/master/3images/30901.png)
* 文件操作方面也沒有替代模塊,只能盡量控制好IO,或者使用內存型(Redis)及文檔型(MongoDB)數據庫。
* time.sleep()在tornado中有替代:`tornado.gen.sleep()`或者`tornado.ioloop.IOLoop.instance().add_timeout`,這在前面代碼已經顯示了。
* smtp發送郵件,推薦改為tornado-smtp-client。
* 對于網絡操作,要使用tornado.httpclient.AsyncHTTPClient。
其它的解決方法,只能看到問題具體說了,甚至沒有很好的解決方法。不過,這里有一個列表,列出了足夠多的庫,供使用者選擇:[Async Client Libraries built on tornado.ioloop](https://github.com/tornadoweb/tornado/wiki/Links),同時這個頁面里面還有很多別的鏈接,都是很好的資源,建議讀者多看看。
教程到這里,讀者是不是要思考一個問題,既然對于mongodb有專門的motor庫來實現異步,前面對于tornado的異步,不管是哪個裝飾器,都感覺麻煩,有沒有專門的庫來實現這種異步呢?這不是異想天開,還真有。也應該有,因為這才體現python的特點。比如[greenlet-tornado](https://github.com/mopub/greenlet-tornado),就是一個不錯的庫。讀者可以瀏覽官方網站深入了解(為什么對mysql那么不積極呢?按理說應該出來好多支持mysql異步的庫才對)。
必須聲明,前面演示如何在tornado中設置異步的代碼,僅僅是演示理解設置方法。在工程實踐中,那個代碼的意義不到。為此,應該有一個近似于實踐的代碼示例。是的,的確應該有。當我正要寫這樣的代碼時候,在網上發現一篇文章,這篇文章阻止了我寫,因為我要寫的那篇文章的作者早就寫好了,而且我認為表述非常到位,示例也詳細。所以,我不得不放棄,轉而推薦給讀者這篇好文章:
舉例:[http://emptysqua.re/blog/refactoring-tornado-coroutines/](http://emptysqua.re/blog/refactoring-tornado-coroutines/)
- 第零章 預備
- 關于Python的故事
- 從小工到專家
- Python安裝
- 集成開發環境
- 第壹章 基本數據類型
- 數和四則運算
- 除法
- 常用數學函數和運算優先級
- 寫一個簡單的程序
- 字符串(1)
- 字符串(2)
- 字符串(3)
- 字符串(4)
- 字符編碼
- 列表(1)
- 列表(2)
- 列表(3)
- 回顧list和str
- 元組
- 字典(1)
- 字典(2)
- 集合(1)
- 集合(2)
- 第貳章 語句和文件
- 運算符
- 語句(1)
- 語句(2)
- 語句(3)
- 語句(4)
- 語句(5)
- 文件(1)
- 文件(2)
- 迭代
- 練習
- 自省
- 第叁章 函數
- 函數(1)
- 函數(2)
- 函數(3)
- 函數(4)
- 函數練習
- 第肆章 類
- 類(1)
- 類(2)
- 類(3)
- 類(4)
- 類(5)
- 多態和封裝
- 特殊方法(1)
- 特殊方法(2)
- 迭代器
- 生成器
- 上下文管理器
- 第伍章 錯誤和異常
- 錯誤和異常(1)
- 錯誤和異常(2)
- 錯誤和異常(3)
- 第陸章 模塊
- 編寫模塊
- 標準庫(1)
- 標準庫(2)
- 標準庫(3)
- 標準庫(4)
- 標準庫(5)
- 標準庫(6)
- 標準庫(7)
- 標準庫(8)
- 第三方庫
- 第柒章 保存數據
- 將數據存入文件
- mysql數據庫(1)
- MySQL數據庫(2)
- mongodb數據庫(1)
- SQLite數據庫
- 電子表格
- 第捌章 用Tornado做網站
- 為做網站而準備
- 分析Hello
- 用tornado做網站(1)
- 用tornado做網站(2)
- 用tornado做網站(3)
- 用tornado做網站(4)
- 用tornado做網站(5)
- 用tornado做網站(6)
- 用tornado做網站(7)
- 第玖章 科學計算
- 為計算做準備
- Pandas使用(1)
- Pandas使用(2)
- 處理股票數據
- 附:網絡文摘
- 如何成為Python高手
- ASCII、Unicode、GBK和UTF-8字符編碼的區別聯系