twisted(1)--何爲異步

  早就想寫一篇文章,總體介紹python的2個異步庫,twisted和tornado。咱們在開發python的tcpserver時候,一般只會用3個庫,twisted、tornado和gevent,其中以twisted和tornado爲表明的異步庫的效率比較高,但對於開發者要求有點高。你們都在討論異步效率高,那到底什麼是異步,爲什麼它的效率比較高呢?世界老是守恆的,異步效率高的同時犧牲了什麼呢?咱們今天就來說講python的異步庫。python

  其實咱們談論的異步庫都是基於計算機模型Event Loop,它不僅僅只有python有,若是你們用過ajax就知道,ajax獲取數據的時候,通常都是異步獲取。其實整個js都是基於eventloop的單線程,好吧,扯遠了。那什麼是Eevent Loop呢?請看下圖react

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

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

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

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

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

 

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

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

  對比歷史的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中,不能大量使用線程。

  一、效率問題,若是用線程,咱們幹嗎還用twisted呢?線程會頻繁切換cpu調度,若是大量使用線程,會極大浪費cpu資源,效率會嚴重降低。

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

  

  好了,這章就先講到這,咱們下一章會繼續講twisted作tcpserver,把上一個flask api 系列的項目引進來,作一個聊天系統。   

相關文章
相關標籤/搜索