python非同步IO與批量請求處理

天府雲創發表於2018-01-17

我們知道,CPU的速度遠遠快於磁碟、網路等IO。在一個執行緒中,CPU執行程式碼的速度極快,然而,一旦遇到IO操作,如讀寫檔案、傳送網路資料時,就需要等待IO操作完成,才能繼續進行下一步操作。這種情況稱為同步IO。

在IO操作的過程中,當前執行緒被掛起,而其他需要CPU執行的程式碼就無法被當前執行緒執行了。

因為一個IO操作就阻塞了當前執行緒,導致其他程式碼無法執行,所以我們必須使用多執行緒或者多程式來併發執行程式碼,為多個使用者服務。每個使用者都會分配一個執行緒,如果遇到IO導致執行緒被掛起,其他使用者的執行緒不受影響。

多執行緒和多程式的模型雖然解決了併發問題,但是系統不能無上限地增加執行緒。由於系統切換執行緒的開銷也很大,所以,一旦執行緒數量過多,CPU的時間就花線上程切換上了,真正執行程式碼的時間就少了,結果導致效能嚴重下降。

由於我們要解決的問題是CPU高速執行能力和IO裝置的龜速嚴重不匹配,多執行緒和多程式只是解決這一問題的一種方法。

另一種解決IO問題的方法是非同步IO。當程式碼需要執行一個耗時的IO操作時,它只發出IO指令,並不等待IO結果,然後就去執行其他程式碼了。一段時間後,當IO返回結果時,再通知CPU進行處理。

可以想象如果按普通順序寫出的程式碼實際上是沒法完成非同步IO的:

do_some_code()
f = open('/path/to/file', 'r')
r = f.read() # <== 執行緒停在此處等待IO操作結果
# IO操作完成後執行緒才能繼續執行:
do_some_code(r)

所以,同步IO模型的程式碼是無法實現非同步IO模型的。

非同步IO模型需要一個訊息迴圈,在訊息迴圈中,主執行緒不斷地重複“讀取訊息-處理訊息”這一過程:

loop = get_event_loop()
while True:
    event = loop.get_event()
    process_event(event)

訊息模型其實早在應用在桌面應用程式中了。一個GUI程式的主執行緒就負責不停地讀取訊息並處理訊息。所有的鍵盤、滑鼠等訊息都被髮送到GUI程式的訊息佇列中,然後由GUI程式的主執行緒處理。

由於GUI執行緒處理鍵盤、滑鼠等訊息的速度非常快,所以使用者感覺不到延遲。某些時候,GUI執行緒在一個訊息處理的過程中遇到問題導致一次訊息處理時間過長,此時,使用者會感覺到整個GUI程式停止響應了,敲鍵盤、點滑鼠都沒有反應。這種情況說明在訊息模型中,處理一個訊息必須非常迅速,否則,主執行緒將無法及時處理訊息佇列中的其他訊息,導致程式看上去停止響應。

訊息模型是如何解決同步IO必須等待IO操作這一問題的呢?當遇到IO操作時,程式碼只負責發出IO請求,不等待IO結果,然後直接結束本輪訊息處理,進入下一輪訊息處理過程。當IO操作完成後,將收到一條“IO完成”的訊息,處理該訊息時就可以直接獲取IO操作結果。

在“發出IO請求”到收到“IO完成”的這段時間裡,同步IO模型下,主執行緒只能掛起,但非同步IO模型下,主執行緒並沒有休息,而是在訊息迴圈中繼續處理其他訊息。這樣,在非同步IO模型下,一個執行緒就可以同時處理多個IO請求,並且沒有切換執行緒的操作。對於大多數IO密集型的應用程式,使用非同步IO將大大提升系統的多工處理能力。

【探索實踐Python的非同步框架】

httpclient,twisted、tornado、gevent,scrapy-redis,asyncio 這些框架你覺得用哪個比較好呢,列舉效能和速度就行了。試過多執行緒檢測幾萬個url要跑幾個小時才檢測的完 效率低 所以就想到了非同步IO或者非同步網路 。

探索之前,先簡單介紹下各種 IO 模型:
最容易做的是阻塞 IO,即讀寫資料時,需要等待操作完成,才能繼續執行。進階的做法就是用多執行緒來處理需要 IO 的部分,缺點是開銷會有些大。
接著是非阻塞 IO,即讀寫資料時,如果暫時不可讀寫,則立刻返回,而不等待。因為不知道什麼時候是可讀寫的,所以輪詢時可能會浪費 CPU 時間。
然後是 IO 複用,即在讀寫資料前,先檢查哪些描述符是可讀寫的,再去讀寫。select 和 poll 就是這樣做的,它們會遍歷所有被監視的描述符,檢視是否滿足,這個檢查的過程是阻塞的。而 epoll、kqueue 和 /dev/poll 則做了些改進,事先註冊需要檢查哪些描述符的哪些事件,當狀態發生變化時,核心會呼叫對應的回撥函式,將這些描述符儲存下來;下次獲取可用的描述符時,直接返回這些發生變化的描述符即可。
再之後是訊號驅動,即描述符就緒時,核心傳送 SIGIO 訊號,再由訊號處理程式去處理這些訊號即可。不過訊號處理的時機是從核心態返回使用者態時,感覺也得把這些事件收集起來才好處理,有點像模擬 IO 複用了。
最後是非同步 IO,即讀寫資料時,只註冊事件,核心完成讀寫後(讀取的資料會複製到使用者態),再呼叫事件處理函式。這整個過程都不會阻塞呼叫執行緒,不過實現它的作業系統比較少,Windows 上有比較成熟的 IOCP,Linux 上的 AIO 則有不少缺點。
雖然真正的非同步 IO 需要中間任何步驟都沒有阻塞,這對於某些只是偶爾需要處理 IO 請求的情況確實有用(比如文字編輯器偶爾儲存一下檔案);但對於伺服器端程式設計的大多數情況而言,它的主執行緒就是用來處理 IO 請求的,如果在空閒時不阻塞在 IO 等待上,也沒有別的事情能做,所以本文就不糾結這個非同步是否名副其實了。

在 Python 2 的時代,高效能的網路程式設計主要是使用 Twisted、Tornado 和 gevent 這三個庫。
我對 Twisted 不熟,只知道它的缺點是比較重,效能相對而言並不算好。
Tornado 平時用得比較多,缺點是寫非同步呼叫時特別麻煩。
gevent 我只能算接觸過,缺點是不太乾淨。
由於它們都各自有一個 IO loop,不好混用,而 Tornado 的 web 框架相對而言比較完善,因此成了我的首選。

而從 Python 3.4 開始,標準庫裡又新增了 asyncio 這個模組。
從原理上來說,它和 Tornado 其實差不多,都是註冊 IO 事件,然後在 IO loop 中等待事件發生,然後呼叫相應的處理函式。
不同之處在於 Python 3 增加了一些新的特性,而 Tornado 需要相容 Python 2,所以寫起來會比較麻煩。
舉例來說,Python 3.3 可以在 generator 中 return 返回值(相當於 raise StopIteration),而 Tornado 中需要 raise 一個 Return 物件。此外,Python 3.3 還增加了 yield from 語法,減輕了在 generator 中處理另一個 generator 的工作量(省去了迴圈和 try … except …)。

不過,雖然 asyncio 有那麼多得天獨厚的優勢,卻不一定比 Tornado 的效能更好,所以我寫個簡單的例子測試一下。
比較方法就是寫個最簡單的 HTTP 伺服器,不做任何檢查,讀取到任何內容都輸出一個 hello world,並斷開連線。
測試的客戶端就懶得寫了,直接用 ab 即可:

1

ab -n 10000 -c 10 "http://0.0.0.0:8000/"

Tornado 版是這樣:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

from tornado.gen import coroutine

from tornado.ioloop import IOLoop

from tornado.tcpserver import TCPServer

 

class Server(TCPServer):

    @coroutine

    def handle_stream(self, stream, address):

        try:

            yield stream.read_bytes(1024, partial=True)

            yield stream.write(b'HTTP 1.0 200 OKrnrnhello world')

        finally:

            stream.close()

 

server = Server()

server.bind(8000)

server.start(1)

IOLoop.current().start()

在我的電腦上大概 4000 QPS。

asyncio 版是這樣:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

import asyncio

 

class Server(asyncio.Protocol):

    def connection_made(self, transport):

        self.transport = transport

 

    def data_received(self, data):

        try:

            self.transport.write(b'HTTP/1.1 200 OKrnrnhello world')

        finally:

            self.transport.close()

 

loop = asyncio.get_event_loop()

server = loop.create_server(Server, '', 8000)

loop.run_until_complete(server)

loop.run_forever()

在我的電腦上大概 3000 QPS,比 Tornado 版慢了一些。此外,asyncio 的 transport 在 write 時不用 yield from,這點可能有些不一致。

asyncio 還有個高階版的 API:

1

2

3

4

5

6

7

8

9

10

11

12

13

import asyncio

 

@asyncio.coroutine

def handle(reader, writer):

    yield from reader.read(1024)

    writer.write(b'HTTP/1.1 200 OKrnrnhello world')

    yield from writer.drain()

    writer.close()

 

loop = asyncio.get_event_loop()

task = asyncio.start_server(handle, '', 8000, loop=loop)

server = loop.run_until_complete(task)

loop.run_forever()

在我的電腦上大概 2200 QPS。這下讀寫都要 yield from 了,一致性上來說會好些。

以框架的效能而言,其實都夠用,開銷都不超過 1 毫秒,而 web 請求一般都需要 10 毫秒的以上的處理時間。
於是順便再測一下和 MySQL 的搭配,即在每個請求內呼叫一下 SELECT 1,然後輸出返回值。
因為自己懶得寫客戶端了,於是就用現成的 tornado_mysql 和 aiomysql 來測試了。原理應該都差不多,傳送寫請求後就返回,等收到可讀事件時再獲取內容。

Tornado 版是這樣:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

from tornado.gen import coroutine

from tornado.ioloop import IOLoop

from tornado.tcpserver import TCPServer

from tornado_mysql import pools

 

class Server(TCPServer):

    @coroutine

    def handle_stream(self, stream, address):

        try:

            yield stream.read_bytes(1024, partial=True)

            cursor = yield POOL.execute(b'SELECT 1')

            data = cursor.fetchone()

            yield stream.write('HTTP/1.1 200 OKrnrn{0[0]}'.format(data).encode())  # Python 3.5 的 bytes 才能用 % 格式化

        finally:

            stream.close()

 

POOL = pools.Pool(

    dict(host='127.0.0.1', port=3306, user='root', passwd='123', db='mysql'),

    max_idle_connections=10,

    max_open_connections=10)

 

server = Server()

server.bind(8000)

server.start(1)

IOLoop.current().start()

在我的電腦上大概 680 QPS。

asyncio 版是這樣:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

import asyncio

 

import aiomysql

 

class Server(asyncio.Protocol):

    def connection_made(self, transport):

        self.transport = transport

 

class Server(asyncio.Protocol):

    def connection_made(self, transport):

        self.transport = transport

 

    def data_received(self, data):

        @asyncio.coroutine

        def handle():

            with (yield from pool) as conn:

                cursor = yield from conn.cursor()

                yield from cursor.execute(b'SELECT 1')

                result = yield from cursor.fetchone()

            try:

                self.transport.write('HTTP/1.1 200 OKrnrn{0[0]}'.format(result).encode())

            finally:

                self.transport.close()

        loop.create_task(handle())  # 或者 asyncio.async(handle())

 

@asyncio.coroutine

def get_pool():

    return(yield from aiomysql.create_pool(host='127.0.0.1', port=3306, user='root', password='123', loop=loop))

 

loop = asyncio.get_event_loop()

pool = loop.run_until_complete(get_pool())

 

server = loop.create_server(Server, '', 8000)

loop.run_until_complete(server)

loop.run_forever()

在我的電腦上大概 1250 QPS,比 Tornado 版快了不少。不過寫起來比較蛋疼,因為 data_received 方法裡不能直接用 yield from。

用 cProfile 看了下,Tornado 版在 tornado.gen 和 functools 模組裡花了不少時間,可能是非同步呼叫過多了吧。
但如果不做非同步庫的開發者,而只就使用者的體驗而言,Tornado 會顯得更加靈活和易用。不過 asyncio 的高階 API 應該也能提供類似的體驗。

順便再用底層 socket 模組寫個伺服器試試。
先用 poll 看看,錯誤處理什麼的就先不做了:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

from functools import partial

import select

import socket

 

class Server:

    def __init__(self):

        self._sock = socket.socket()

        self._poll = select.poll()

        self._handlers = {}

        self._fd_events = {}

 

    def start(self):

        sock = self._sock

        sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

        sock.setblocking(0)

        sock.bind(('', 8000))

        sock.listen(100)

 

        handlers = self._handlers

        poll = self._poll

        self.add_handler(sock.fileno(), self._accept, select.POLLIN)

 

        while True:

            poll_events = poll.poll(1)

            for fd, event in poll_events:

                handler = handlers.get(fd)

                if handler:

                    handler()

 

    def _accept(self):

        for i in range(100):

            try:

                conn, address = self._sock.accept()

            except OSError:

                break

            else:

                conn.setblocking(0)

                fd = conn.fileno()

                self.add_handler(fd, partial(self._read, conn), select.POLLIN)

 

    def _read(self, conn):

        fd = conn.fileno()

        self.remove_handler(fd)

        try:

            conn.recv(1024)

        except:

            conn.close()

            raise

        else:

            self.add_handler(fd, partial(self._write, conn), select.POLLOUT)

 

    def _write(self, conn):

        fd = conn.fileno()

        self.remove_handler(fd)

        try:

            conn.send(b'HTTP 1.0 200 OKrnrnhello world')

        finally:

            conn.close()

 

    def add_handler(self, fd, handler, event):

        self._handlers[fd] = handler

        self.register(fd, event)

 

    def remove_handler(self, fd):

        self._handlers.pop(fd, None)

        self.unregister(fd)

 

    def register(self, fd, event):

        if fd in self._fd_events:

            raise IOError("fd %s already registered" % fd)

        self._poll.register(fd, event)

        self._fd_events[fd] = event

 

    def unregister(self, fd):

        event = self._fd_events.pop(fd, None)

        if event is not None:

            self._poll.unregister(fd)

 

Server().start()

在我的電腦上大概 7700 QPS,優勢巨大。

再用 kqueue 試試(我用的是 OS X):

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

from functools import partial

import select

import socket

 

class Server:

    def __init__(self):

        self._sock = socket.socket()

        self._kqueue = select.kqueue()

        self._handlers = {}

        self._fd_events = {}

 

    def start(self):

        sock = self._sock

        sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

        sock.setblocking(0)

        sock.bind(('', 8000))

        sock.listen(100)

 

        self.add_handler(sock.fileno(), self._accept, select.KQ_FILTER_READ)

        handlers = self._handlers

 

        while True:

            kevents = self._kqueue.control(None, 1000, 1)

            for kevent in kevents:

                fd = kevent.ident

                handler = handlers.get(fd)

                if handler:

                    handler()

 

    def _accept(self):

        for i in range(100):

            try:

                conn, address = self._sock.accept()

            except OSError:

                break

            else:

                conn.setblocking(0)

                fd = conn.fileno()

                self.add_handler(fd, partial(self._read, conn), select.KQ_FILTER_READ)

 

    def _read(self, conn):

        fd = conn.fileno()

        self.remove_handler(fd)

        try:

            conn.recv(1024)

        except:

            conn.close()

            raise

        else:

            self.add_handler(fd, partial(self._write, conn), select.KQ_FILTER_WRITE)

 

    def _write(self, conn):

        fd = conn.fileno()

        self.remove_handler(fd)

        try:

            conn.send(b'HTTP 1.0 200 OKrnrnhello world')

        finally:

            conn.close()

 

    def add_handler(self, fd, handler, event):

        self._handlers[fd] = handler

        self.register(fd, event)

 

    def remove_handler(self, fd):

        self._handlers.pop(fd, None)

        self.unregister(fd)

 

    def register(self, fd, event):

        if fd in self._fd_events:

            raise IOError("fd %s already registered" % fd)

        self._control(fd, event, select.KQ_EV_ADD)

        self._fd_events[fd] = event

 

    def unregister(self, fd):

        event = self._fd_events.pop(fd, None)

        if event is not None:

            self._control(fd, event, select.KQ_EV_DELETE)

 

    def _control(self, fd, event, flags):

        change_list = (select.kevent(fd, event, flags),)

        self._kqueue.control(change_list, 0)

 

Server().start()

在我的電腦上大概 7200 QPS,比 poll 版稍慢。不過因為只有 10 個併發連線,而且沒有慢速網路的影響,所以 poll 的效能好並不奇怪。

再試試 Python 3.4 新增的 selectors 模組,它的 DefaultSelector 會自動選擇所在平臺最高效的實現,asyncio 就用到了這個模組。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

import selectors

import socket

 

class Server:

    def __init__(self):

        self._sock = socket.socket()

        self._selector = selectors.DefaultSelector()

 

    def start(self):

        sock = self._sock

        sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

        sock.setblocking(0)

        sock.bind(('', 8000))

        sock.listen(100)

 

        selector = self._selector

        self.add_handler(sock.fileno(), self._accept, selectors.EVENT_READ)

 

        while True:

            events = selector.select(1)

            for key, event in events:

                handler, data = key.data

                if data:

                    handler(**data)

                else:

                    handler()

 

    def _accept(self):

        for i in range(100):

            try:

                conn, address = self._sock.accept()

            except OSError:

                break

            else:

                conn.setblocking(0)

                fd = conn.fileno()

                self.add_handler(fd, self._read, selectors.EVENT_READ, {'conn': conn})

 

    def _read(self, conn):

        fd = conn.fileno()

        self.remove_handler(fd)

        try:

            conn.recv(1024)

        except:

            conn.close()

            raise

        else:

            self.add_handler(fd, self._write, selectors.EVENT_WRITE, {'conn': conn})

 

    def _write(self, conn):

        fd = conn.fileno()

        self.remove_handler(fd)

        try:

            conn.send(b'HTTP 1.0 200 OKrnrnhello world')

        finally:

            conn.close()

 

    def add_handler(self, fd, handler, event, data=None):

        self._selector.register(fd, event, (handler, data))

 

    def remove_handler(self, fd):

        self._selector.unregister(fd)

 

Server().start()

在我的電腦上大概 6100 QPS,成績也還不錯。

從這些測試來看,如果想自己實現一個捨棄了一些功能和相容性的 Tornado,應該能比它稍快一點,不過似乎沒多大必要。
所以暫時不糾結效能了,還是從使用的便利性上來考慮。Tornado 可以用 yield 取代 callback,我們也來實現這個 feature。

實現前先得了解下 yield。
當一個函式內部出現了 yield 語句時,它就不再是一個單純的函式了,而是一個生成器函式,呼叫它並不會執行它的程式碼,而是返回一個生成器。
呼叫這個生成器的 send 方法時,才會執行內部的程式碼。當執行到 yield 時,這個 send 方法就返回了,呼叫者可以得到其返回值。
send 方法在第一次呼叫時,引數必須為 None。Python 2 中可以用它的 next 方法,Python 3 中改成了 __next__ 方法,還可以用內建的 next 函式來呼叫。
send 方法可以被多次呼叫,引數會作為 yield 的返回值,回到生成器內上一次執行的地方,並繼續執行下去。
當生成器的程式碼執行完時,會丟擲一個 StopIteration 的異常。Python 3.3 開始可以在生成器裡使用 return,返回值可以從 StopIteration 異常的 value 屬性獲取。
for … in … 迴圈會自動捕獲 StopIteration 異常,並作為迴圈停止的條件。

由此可見,yield 可以用於跳轉。而我們要做的,則是在遇到 IO 請求時,用 yield 返回 IO loop;當事件發生時,找到對應的生成器,用 send 方法繼續執行即可。
為了簡單起見,我就在 poll 版的基礎上進行改造了:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

from collections import deque

import select

import socket

from types import GeneratorType

 

class Stream:

    def __init__(self, sock, loop):

        sock.setblocking(0)

        self._sock = sock

        self._loop = loop

 

    def close(self):

        self._sock.close()

 

    def read(self, size=1024):

        sock = self._sock

        fd = sock.fileno()

        try:

            data = sock.recv(size)

        except OSError as e:

            if e.errno == socket.EAGAIN or socket.EWOULDBLOCK:

                self._loop.add_handler(fd, self.read(size), select.POLLIN)

                yield

            else:

                raise

        else:

            return data

        finally:

            self._loop.remove_handler(fd)

 

    def write(self, data):

        sock = self._sock

        fd = sock.fileno()

        try:

            try:

                sent_bytes = sock.send(data)

            except OSError as e:

                if e.errno not in (socket.EAGAIN, socket.EWOULDBLOCK):

                    raise

            else:

                if sent_bytes == len(data):

                    return

                data = data[sent_bytes:]

 

            self._loop.add_handler(fd, self.write(data), select.POLLOUT)

            yield

 

            while data:

                try:

                    sent_bytes = sock.send(data)

                except OSError as e:

                    if e.errno not in (socket.EAGAIN, socket.EWOULDBLOCK):

                        raise

                else:

                    if sent_bytes == len(data):

                        return

                    data = data[sent_bytes:]

                yield

        finally:

            self._loop.remove_handler(fd)

 

class IOLoop:

    def __init__(self):

        self._poll = select.poll()

        self._handlers = {}

        self._fd_events = {}

 

    def start(self):

        handlers = self._handlers

        poll = self._poll

 

        while True:

            poll_events = poll.poll(1)

            for fd, event in poll_events:

                handler = handlers.get(fd)

                if handler:

                    if callable(handler):

                        handler()

                    else:

                        stack = handler

                        while True:

                            generator, value = stack[-1]

                            try:

                                value = generator.send(value)

                                if isinstance(value, GeneratorType):

                                    stack.append([value, None])

                                else:

                                    break

                            except StopIteration as e:

                                stack.pop()

                                if stack:

                                    stack[-1][-1] = e.value

                                else:

                                    break

 

    def add_handler(self, fd, handler, event):

        if isinstance(handler, GeneratorType):

            self._handlers[fd] = deque([[handler, None]])

        else:

            self._handlers[fd] = handler

        self.register(fd, event)

 

    def remove_handler(self, fd):

        self._handlers.pop(fd, None)

        self.unregister(fd)

 

    def update_handler(self, fd, event):

        self.modify(fd, event)

 

    def register(self, fd, event):

        if fd in self._fd_events:

            raise IOError("fd %s already registered" % fd)

        self._poll.register(fd, event)

        self._fd_events[fd] = event

 

    def unregister(self, fd):

        event = self._fd_events.pop(fd, None)

        if event is not None:

            self._poll.unregister(fd)

 

    def modify(self, fd, event):

        self._poll.modify(fd, event)

        self._fd_events[fd] = event

 

class Server:

    def __init__(self):

        self._sock = socket.socket()

        self._loop = IOLoop()

        self._stream = Stream(self._sock, self._loop)

 

    def start(self):

        sock = self._sock

        sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

        sock.setblocking(0)

        sock.bind(('', 8000))

        sock.listen(100)

 

        self._loop.add_handler(sock.fileno(), self._accept, select.POLLIN)

        self._loop.start()

 

    def _accept(self):

        for i in range(100):

            try:

                conn, address = self._sock.accept()

            except OSError:

                break

            else:

                stream = Stream(conn, self._loop)

                fd = conn.fileno()

                self._loop.add_handler(fd, self._handle(stream), select.POLLIN)

 

    def _handle(self, stream):

        yield stream.read()

        yield stream.write(b'HTTP 1.0 200 OK\r\n\r\nhello world')

 

Server().start()

在我的電腦上大概 5300 QPS。

雖然成績比較尷尬,但畢竟用起來比前一個版本好多了。至於慢的原因,我估計是自己維護了一個堆疊的原因(也可能是有什麼 bug,畢竟寫這個感覺太跳躍了,能執行起來就謝天謝地了)。
實現時做了兩點假設:

  1. handler 為 generator 時,視為非同步方法。
  2. 在非同步方法中 yield None 時,視為等待 IO;yield / yield from 非同步方法時,則是等待方法返回。

實現細節也沒什麼好說的了,只是覺得在實現 Stream 的 read / write 方法時,呼叫 IOLoop.add_handler 方法不太優雅。其實可以直接 yield 一個 fd 和 event,在 IOLoop.start 方法中再去註冊。不過這個重構其實蠻小的,我就不再貼一次程式碼了,感興趣的可以自己試試。

於是這次初探就到此為止了,有空我也許會繼續完善它。至少這次探索,讓我覺得 Python 3 還是蠻有意思的。

【參考文件】

1、非同步IO - 廖雪峰的官方網站 https://www.liaoxuefeng.com/wiki/0014316089557264a6b348958f449949df42a6d3a2e542c000/00143208573480558080fa77514407cb23834c78c6c7309000

2、非同步案例 - Python - 伯樂線上 http://python.jobbole.com/tag/%E5%BC%82%E6%AD%A5/

 

相關文章