Twisted簡介與初步使用

我們在開發python的tcpserver時候,通常只會用3個庫,twisted、tornado和gevent,其中以twisted和tornado爲代表的異步庫的效率比較高,但對於開發者要求有點高。大家都在討論異步效率高,那到底什麼是異步,爲何它的效率比較高呢?世界總是守恆的,異步效率高的同時犧牲了什麼呢?我們今天就來講講python的異步庫。

  其實我們談論的異步庫都是基於計算機模型Event Loop,它不單單隻有python有,如果大家用過ajax就知道,ajax獲取數據的時候,一般都是異步獲取。其實整個js都是基於eventloop的單線程,好吧,扯遠了。那什麼是Eevent Loop呢?請看下圖

  我們知道,每一個程序運行都會開啓一個進程,在tcpserver服務器歷史上,主要有3種方式來處理客戶端來的連接。

  爲了方便說明,我們把tcpserver想象成對銀行辦理業務的過程,你每次去銀行辦理業務的時候,其實真正辦理業務的時間並不長,其中很多時候,銀行的工作人員也在等待,比如她操作一筆業務,電腦還沒有及時反應過來,她沒事可做,只能等待;打印各種文件的時候,也在等待。這其實跟我們的tcpserver是一樣的,很多應用,我們的tcpserver一直在等待。

  第一,阻塞排隊。銀行只開通一個窗口,每個人過來,都要排隊,每一個人都要等待,其中還有很多時候,銀行的工作人員在等電腦、打印機的操作時間。這種方式效率最低下。

  第二,子進程。每次來一個客戶,銀行都開啓一個窗口,專門接待,但銀行的窗口不是無限的,每開啓一個窗口,都有代價。這種方式比上面好了一些,但效率還不是那麼高。

  第三,線程。銀行看到每個業務員雖然一直在忙活,但中間等待時間過長,效率提高不上來。於是,領導規定,每個業務員同時處理10個客戶(1個進程開始10個線程),在處理客戶1的空餘時間,再處理客戶2,或者其他的。嗯,貌似效率提高了,但業務員同時接這麼多客戶,極其容易出錯(線程模式,確實容易出錯,而且還不好控制,通常線程都只是處理比較單一、簡單的任務)。

 

  好了,經過對歷史問題的研究,銀行終於想到了終極大法,異步。銀行請了機器人做業務員,並且把所有的客戶都圍成一個圈(這個圈就是eventloop),機器人站在這個圈的中間,不停的旋轉(無限循環)。機器人每次接到一個客戶,都讓客戶加入到這個圈子裏。然後就開始處理業務,處理業務,那旋轉暫停,如果在處理這個業務的時候,遇到任何忙等待行爲,比如操作打印機等待、操作電腦時等待,都會先把這個業務掛起來,保存好(保存上下文環境,其實可以想象成壓棧),然後繼續旋轉,如果有其他業務過來,處理之,繼續上述行爲。這時候,有個業務等待完畢,發送信號給機器人,機器人把剛纔掛起的這個業務環境(把保存好的上下文環境拉出來,想象成出棧),然後繼續處理,一直到處理完爲止。

  整個過程就是無限循環,遇到事件就處理,如果這個事件需要等待,就掛起,繼續循環,如果等待完畢,發送信號給循環,繼續處理,完畢後,繼續循環。這就是異步。

  對比歷史的3個過程,異步是不是效率明顯要比之前的高很多?但是也有代價,尤其對程序員要求比較高,什麼時候該保存上下文?什麼時候出來?出錯的時候,如何處理?等等,這個以後我們會逐漸介紹這其中的問題。

  

  

    下面我們回到實際的twisted,這個圖是官方引用圖,我覺得非常好的詮釋了twisted的運行過程。通過這個圖,再結合我上面的例子,我想大家對twisted的運行過程有個基本瞭解了。

  實際上,這個reactor loop就是整合twisted最核心的東西,所有的事件都在這個“圈”上,而在此基礎上,再加上socket,就是接受網絡客戶端數據的過程。這個圈在沒有socket的情況下,也可以工作。以後我們會遇到twisted結合rabbitmq的情況,rabbitmq的消費者也是一個"圈",其實就是把這個"圈"套在twisted的哪個"圈"上,只不過twisted的任何事件,都需要異步化。

  上面說了這麼多概念,我們就用代碼試試twisted。我發現網上很多博客開始介紹twisted,往往一大堆代碼,新手都不知道怎麼入手,這對新手來說,是一個難題。我們今天就嘗試解決這個難題。

from twisted.internet import reactor
reactor.run()

  代碼如上,就1行代碼,直接運行,這時候這個"圈"就運行起來了。沒有socket,不能接受客戶端寫入數據。

  在此基礎上,加一點料。

 

import time

def hello():
    print("Hello world!===>" + str(int(time.time())))

from twisted.internet import reactor
reactor.callWhenRunning(hello)
reactor.callLater(3, hello)
reactor.run()

 

 

 

  看代碼,我想,你就是不懂twisted,看字面意思,也知道這怎麼回事了吧。callWhenRunning,就是reactor開始運行的時候,就觸發hello函數;callLater就是3秒以後再觸發一次。看一下結果

/usr/bin/python3.5 /home/yudahai/PycharmProjects/test0001/test001.py
Hello world!===>1466129667
Hello world!===>1466129670

  結果也這樣,是不是很簡單?對,單純的reactor確實非常簡單。我們多嘗試複雜點的任務看看。

 

 

 

import time

def hello(name):
    print("Hello world!===>" + name + '===>' + str(int(time.time())))

from twisted.internet import reactor, task

task1 = task.LoopingCall(hello, 'ding')
task1.start(10)

reactor.callWhenRunning(hello, 'yudahai')
reactor.callLater(3, hello, 'yuyue')

reactor.run()

 

 

 

  這面在函數裏面,多加了一個參數,又在其中,加了一個循環任務taks1,task1每10秒運行一次。task用twisted會經常用到,因爲我們會輪詢檢測每個連接上來的客戶端意外斷線的情況,這時候就要用到task。好了,看看結果。

 

 

/usr/bin/python3.5 /home/yudahai/PycharmProjects/test0001/test001.py
Hello world!===>ding===>1466130033
Hello world!===>yudahai===>1466130033
Hello world!===>yuyue===>1466130036
Hello world!===>ding===>1466130043
Hello world!===>ding===>1466130053
Hello world!===>ding===>1466130063
Hello world!===>ding===>1466130073
Hello world!===>ding===>1466130083
Hello world!===>ding===>1466130093
Hello world!===>ding===>1466130103

 

 

 

  看到結果,大家應該對日常twisted這個"圈"會基本使用了吧。

  嗯,基本使用會了,但貌似這個很簡單呀,沒有網上所說的,twisted如何難呀?貌似也沒看到中間有任何代價呀?爲什麼一定要異步呢?爲什麼中間不能阻塞呢?好吧,上面的例子確實看不出來,我們來看如下一段代碼,看看阻塞的效果。大家都知道,我們這邊是不能訪問google網站的,我們在中間試試訪問google網站,看看效果會咋樣。

 

 

 

import time
import requests


def hello(name):
    print("Hello world!===>" + name + '===>' + str(int(time.time())))


def request_google():
    res = requests.get('http://www.google.com')
    return res

from twisted.internet import reactor, task

reactor.callWhenRunning(hello, 'yudahai')

reactor.callLater(1, request_google)

reactor.callLater(3, hello, 'yuyue')

reactor.run()

 

 

 

  我在開始的時候運行一個打印任務,非阻塞,然後1秒之後,發送一個指向google的請求,到第3秒的時候,再執行打印。看看結果

 

 

/usr/bin/python3.5 /home/yudahai/PycharmProjects/test0001/test001.py
Hello world!===>yudahai===>1466130855
Hello world!===>yuyue===>1466130984
Unhandled Error
Traceback (most recent call last):
  File "/home/yudahai/PycharmProjects/test0001/test001.py", line 21, in <module>
    reactor.run()
  File "/usr/local/lib/python3.5/dist-packages/twisted/internet/base.py", line 1194, in run
    self.mainLoop()
  File "/usr/local/lib/python3.5/dist-packages/twisted/internet/base.py", line 1203, in mainLoop
    self.runUntilCurrent()
--- <exception caught here> ---
  File "/usr/local/lib/python3.5/dist-packages/twisted/internet/base.py", line 825, in runUntilCurrent
    call.func(*call.args, **call.kw)
  File "/home/yudahai/PycharmProjects/test0001/test001.py", line 10, in request_google
    res = requests.get('http://www.google.com')
  File "/usr/local/lib/python3.5/dist-packages/requests/api.py", line 67, in get
    return request('get', url, params=params, **kwargs)
  File "/usr/local/lib/python3.5/dist-packages/requests/api.py", line 53, in request
    return session.request(method=method, url=url, **kwargs)
  File "/usr/local/lib/python3.5/dist-packages/requests/sessions.py", line 468, in request
    resp = self.send(prep, **send_kwargs)
  File "/usr/local/lib/python3.5/dist-packages/requests/sessions.py", line 576, in send
    r = adapter.send(request, **kwargs)
  File "/usr/local/lib/python3.5/dist-packages/requests/adapters.py", line 437, in send
    raise ConnectionError(e, request=request)
requests.exceptions.ConnectionError: HTTPConnectionPool(host='www.google.com', port=80): Max retries exceeded with url: / (Caused by NewConnectionError('<requests.packages.urllib3.connection.HTTPConnection object at 0x7fc189c69e48>: Failed to establish a new connection: [Errno 101] Network is unreachable',))

 

 

  看看2個打印之間的間隔,大概相差了130秒,也就是說,中間的130秒,這個程序什麼事都沒有幹,僅僅是等待。當然,我這個例子有點極端,但在實際過程中,訪問數據庫,訪問網絡,都有可能阻塞住。程序一旦阻塞,效率會極其底下。

  那該如何解決呢?這邊有2種方法,一個是用twisted自帶的httpclient進行訪問,twisted自帶的httpclient由於是異步的,不會阻塞住整個reactor的運行;其次是用線程的方式運行,注意,這裏的線程不是python普通線程,是twisted自帶的線程,它訪問完畢的時候,會發送一個信號給reactor。下面我們分別用2中方法試試吧。

 

 

# coding:utf-8
import time
from twisted.web.client import Agent
from twisted.web.http_headers import Headers
from twisted.internet import reactor, task, defer


def hello(name):
    print("Hello world!===>" + name + '===>' + str(int(time.time())))


@defer.inlineCallbacks
def request_google():
    agent = Agent(reactor)
    try:
        result = yield agent.request('GET', 'http://www.google.com', Headers({'User-Agent': ['Twisted Web Client Example']}), None)
    except Exception as e:
        print e
        return 
    print(result)


reactor.callWhenRunning(hello, 'yudahai')

reactor.callLater(1, request_google)

reactor.callLater(3, hello, 'yuyue')

reactor.run()

 

 

  這就是非阻塞版本的代碼,其中,request返回的是一個延遲對象,所以不會阻塞住reactor,看看結果。

/usr/bin/python2.7 /home/yudahai/PycharmProjects/test0001/test001.py
Hello world!===>yudahai===>1466386544
Hello world!===>yuyue===>1466386547
User timeout caused connection failure.

  除了訪問google的,其他的都按時回來,訪問谷歌的並沒有阻塞reactor。

  上面用非阻塞的方式訪問過了,其實在現實過程中,我們很多庫沒有非阻塞模式的api,要非阻塞模式,一定要返回twisted的defer對象,如果寫一個庫,還要針對twisted寫一個異步版,這肯定強人所難。而且很多時候,哪怕自己的函數,如果不是特別複雜,都可以用線程模式,twisted本身訪問數據庫就是線程模式。我們來看看線程模式的代碼。

 

 

# coding:utf-8
import time
import requests
from twisted.internet import reactor, task, defer


def hello(name):
    print("Hello world!===>" + name + '===>' + str(int(time.time())))


def request_google():
    try:
        result = requests.get('http://www.google.com', timeout=10)
    except Exception as e:
        print e
        return 
    print(result)


reactor.callWhenRunning(hello, 'yudahai')

reactor.callInThread(request_google)

reactor.callLater(3, hello, 'yuyue')

reactor.run()

 

 

  代碼很簡單,就是把request_google換成線程模式。看看結果。

 

 

/usr/bin/python2.7 /home/yudahai/PycharmProjects/test0001/test001.py
Hello world!===>yudahai===>1466387418
Hello world!===>yuyue===>1466387421
HTTPConnectionPool(host='www.google.com', port=80): Max retries exceeded with url: / (Caused by NewConnectionError('<requests.packages.urllib3.connection.HTTPConnection object at 0x7fc9da0b1ad0>: Failed to establish a new connection: [Errno 101] Network is unreachable',))

 

 

是不是也同樣達到目的了?嗯,這時候,大家可能會在想,既然線程也可以把阻塞代碼線程化,爲啥還直接寫異步代碼呢?異步代碼那麼難寫、難看還容易出錯。

  這邊其實有幾個理由,在twisted中,不能大量使用線程。

  1、效率問題,如果用線程,我們幹嘛還用twisted呢?線程會頻繁切換cpu調度,如果大量使用線程,會極大浪費cpu資源,效率會嚴重下降。

  2、線程安全,如果第一個問題稍微還有點理由的話,那線程安全問題絕對不能忽視了。比如用twisted接受網絡數據的時候,是非線程安全的,如果用線程模式接受數據,會引起程序崩潰。twisted只有極少數的api支持線程。其實用的最多的例子就是消息隊列的接受系統,很多初級程序員會用線程模式來做消息隊列的接受方式,一開始沒問題,結果運行一段時間以後,就會發現程序不能正常接受數據了,而且還不報錯。twisted官方也建議大家,只要有異步庫,一定優先使用異步庫,線程只是做非常簡單而且不是頻繁的操作。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章