用一個簡易的 web chat 說說 Python、Golang、Nodejs 的異步

在 web 編程中,常常有業務須要在處理請求時作異步操做,好比耗時太長的 IO 操做,等異步執行完成以後再結束請求返回 response 到 client,在這個過程當中 client 和 server 一直保持着鏈接不釋放,也就是當前請求在從 client 的角度看一直處於阻塞狀態,直到請求結束。javascript

之因此稱之爲異步,最重要的特徵就是 server 能夠繼續處理其餘 request 而不被阻塞html

不一樣語言在處理這種異步場景的方式是大相徑庭的,常見的處理策略有:消息共享(異步任務隊列)、多線程多進程、event(linux signals,nodejs event loop)、協程 coroutine(返回 Future、Promise 表明程序執行的將來狀態),其中 coroutine 是應用最普遍的,這也是今天此篇的主題。java

什麼是 coroutine?簡單來講就是一段能夠在特定時刻自由被 suspend、execute、kill 的 program。程序對 coroutine 的控制就像操做系統對 process 的控制,可是代價要低廉的多。這也是不少語言都支持用 coroutine 的方式進行異步操做的一個重要緣由,其中就包括 Golang、Python、JavaScript(ES6)、Erlang 等。node

Talk is cheap, show me your code. 在此咱們用一個很是簡單的 web chat demo app 來一塊兒表一表 Golang、Python、Nodejs 中的異步。python

Chat demo app 的簡單描述

Demo 只是爲了說明 coroutine 在不一樣語言是如何應用的,於是場景很是簡單:一個內容輸入框,任意 client 發送的消息都能在其餘 client 顯示。 linux

項目地址git

github.com/zhyq0826/ch…程序員

Chat demo app 的工做原理

兩個主要的 API:github

  1. /a/message/new 用於消息發送,在此稱之爲 message-new
  2. /a/message/updates 用於消息接受,在此稱之爲 message-update

Client 經過 message-update 從 server 端獲取最新的消息,若是沒有新消息,當次 request 就被掛起,等待新消息的發送,當有新消息來臨,獲取最新消息以後,斷開 connection,必定間隔以後從新請求 server 繼續獲取新的消息,並重復以前的過程。golang

因爲 message-update 從 server 獲取消息的時候有可能須要較長時間的等待,server 會一直持有 client 的鏈接不釋放,於是要求來自 message-update client 的請求不能阻塞 server 處理其餘請求,而且 message-update 在沒有消息到達時須要一直掛起。

Server 處理 message-update 的過程就是一個異步的過程

Python 的實現

Python 中用 yield 來實現 coroutine,可是想要在 web 中實現 coroutine 是須要特別處理,在此咱們用了 tornado 這個支持 asynchronous network 的 web framework 來實現 message-update 的處理。

Tornado 中一個 Future 表明的是將來的一個結果,在一次異步請求過程當中,yield 會解析 Future,若是 Future 未完成,請求就會繼續等待。

@gen.coroutine #1
def post(self):
    cursor = self.get_argument("cursor", None)
    # Save the future returned by wait_for_messages so we can cancel
    # it in wait_for_messages
    self.future = GLOBAL_MESSAGE_BUFFER.wait_for_messages(cursor=cursor)
    messages = yield self.future #2
    if self.request.connection.stream.closed():
        return
    self.write(dict(messages=messages))複製代碼

#1 出經過 tornado 特有的 gen.coroutine 讓當前請求支持 coroutine,#2 是當前請求等待的將來的執行結果,每一個 message-update client 都經過 GLOBAL_MESSAGE_BUFFER.wait_for_messages 的調用生成一個 future,而後加入消息等待的列表,只要 future 未解析完成,請求會一直掛起,tornado 就是經過 yield 和 future 的配合來完成一次異步請求的。

理解 yield 是如何等待 future 完成的過程其實就是理解 Python generator 如何解析的過程,細節咱們有機會在表。

Golang 的實現

Golang 天生就在語言層面支持了 coroutine go func() 就能夠開啓 coroutine 的執行,是否是很簡單,是否是很刺激,比起 tornado 必須特別處理要賞心悅目的多,並且 go 自帶的 net/http 包實現的 http 請求又天生支持 coroutine,徹底不須要相似 tornado 這種第三方 library 來支持了(此處爲 Python 2 )。Golang 比 Python 更牛逼的地方在於支持 coroutine 之間使用 channel 進行通訊,是否是更刺激。

func MessageUpdatesHandler(w http.ResponseWriter, r *http.Request) {
    client := Client{id: uuid.NewV4().String(), c: make(chan []byte)} #1
    messageBuffer.NewWaiter(&client) #2
    msg := <-client.c //掛起請求等待消息來臨 #3
    w.Header().Set("Content-Type", "application/json")
    w.Write(msg)
}複製代碼

#1 爲每一個 client 生成 一個惟一的身份和 channel,而後 client 加入消息 #2 等待列表等候消息的來臨,#3 就是掛起請求的關鍵點:等待 channel 的消息。Channel 的通訊默認就是阻塞的,即當前 message-update 這個 coroutine 會被 #3 的等待而掛起不會執行,也就達到了 client 鏈接不能斷的要求。

Nodejs 的實現

Nodejs 天生異步,經過 callback 來完成異步通知的接收和執行。爲了演示方便咱們用了 express ,在 express 中若是一個請求不主動調用 res.endres.sendres.json 請求就不會結束。在 nodejs 中請求如何才能知道消息到達了,須要 response?Python 咱們用了 Future,Golang 用了 channel,Nodejs 實現也毫不僅僅只有一種,在此咱們用了事件Promise

Promise 相似 Future,表明的是一次將來的執行,而且在執行完成以後經過 resolve 和 reject 來完成執行結果的通知,then 或 catch 中獲取執行結果。經過 Promise 能有效解決 nodejs 中回調嵌套以及異步執行錯誤沒法外拋的問題。

Promise 做爲一種規範,nodejs 中有多種第三方庫都作了實現,在次咱們用了 bluebird 這個 library。

事件是 nodejs 中經常使用的編程模型,熟悉 JavaScript 的同窗應該很瞭解了,在此不細表。

app.post('/a/message/updates', function(req, res){
    var p = new Promise(function(resolve, reject){
        messageBuffer.messageEmitter.on("newMessage", function(data){  #1
            resolve(data); #2
        });
    });
    var client = makeClient(uuidv4(), p);
    messageBuffer.newWaiter(client);
    p.then(function(data){  #3
        res.set('Content-Type', 'application/json');
        res.json({'messages': data});
    });
});複製代碼

每一個 message-update client 都會生成一個 Promise,而且 Promise 在消息來臨事件 newMessage #1 觸發之後執行 Promise 的 resolve #2 來告知當前 client 消息來臨。

小結

三種語言實現異步的策略是不盡相同的,其中 Golang 的最容易理解也最容易實現,這徹底得意於 go 天生對 coroutine 的支持以及強大的 channel 通訊。文中 Python 的實現是基於 Python 2,在 Python 3 中 coroutine 的使用有了很大的改善,可是相比 Golang 仍是美中不足。Nodejs 做爲天生的異步後端 JavaScript,想要徹底使用 Promise 來發揮其優點仍是須要不少技巧來讓整個調用棧都完美支持,不過 ES6 中的 yield,ES7 中的 await/async 對異步的操做都有了很大改善,他們的處理方式神似 Python(聽說 ES6 草案的實現就是一羣 Python 程序員)。

下次須要異步支持的項目,你會用哪一個語言?

Python 的例子改自 tornado github.com/tornadoweb/…

chat-app 地址 github.com/zhyq0826/ch…

Promise Bluebird bluebirdjs.com/docs/gettin…

tornado tornado.readthedocs.io/en/stable/g…

Golang channel tour.golang.org/concurrency…

掃碼關注 wecatch,獲取最新文章信息

相關文章
相關標籤/搜索