Async I/O and Python   在Python中的異步IO (1)

此文翻譯自 Mark McLoughlin 的 博客 

Async I/O and Python

原文:http://blogs.gnome.org/markmc/2013/06/04/async-io-and-python/



When you’re working on OpenStack, you’ll probably hear a lot of references to ‘async I/O’ and how eventlet is the library we use for this in OpenStack.

使用OpenStack時,可能會聽說過異步IO以及如何在OpenStack中使用eventlet。


But, well … what exactly is this mysterious ‘asynchronous I/O’ thing?

The first thing to think about is what happens when a process calls a system call like write(). If there’s room in the write buffer, then the data gets copied into kernel space and the system call returns immediately.

那麼,異步IO有何奇妙之處呢?首先需要考慮的是當一個進程調用寫操作時候,如果寫緩衝區有足夠空間,那麼數據複製到系統內核空間,並且系統調用後會立即返回。



But if there isn’t room in the write buffer, what happens then? The default behaviour is that the kernel will put the process to sleep until there is room available. In the case of sockets and pipes, space in the buffer usually becomes available when the other side reads the data you’ve sent.

但是如果沒有足夠的寫緩衝區,會怎麼樣?默認的內核行爲是讓進程休眠,直道有足夠的可用空間。在這種類型的接口和管道中,當其他端讀取完你發送的數據時緩衝區空間經常是可用的。



The trouble with this is that we usually would prefer the process to be doing something useful while waiting for space to become available, rather than just sleeping. Maybe this is an API server and there are new connections waiting to be accepted. How can we process those new connections rather than sleeping?

問題是我們經常在等待空間可用的過程中,需要做一些事情,而不是讓它休眠。例如可以讓API服務器在等待中也允許連接,我們怎麼能處理新的連接而不是讓它休眠呢?


One answer is to use multiple threads or processes – maybe it doesn’t matter if a single thread or process is blocked on some I/O if you have lots of other threads or processes doing work in parallel.

一個答案是線程或進程 -- 當你開闢很多進程和線程後,或許單線程或單進程進入阻塞模式IO的時候不會影響整個程序。



But, actually, the most common answer is to use non-blocking I/O operations. The idea is that rather than having the kernel put the process to sleep when no space is available in the write buffer, the kernel should just return a “try again later” error. We then using the select() system call to find out when space has become available and the file is writable again.

但是事實上,更常見的答案是使用非阻塞IO操作。思路是當寫空間不夠內核把進程設置爲休眠狀態,內核需要返回一個“請重試”的錯誤。接下來我們就可以使用系統的select()模式調用並且等到空間可用時再次嘗試寫操作。


Below are a number of examples of how to implement a non-blocking write. For each example, you can run a simple socket server on a remote machine to test against:

下面是幾個例子實現非阻塞寫操作。例如你可以遠程主機上運行一個簡單的soket服務器:

$> ssh -L 1234:localhost:1234 some.remote.host 'ncat -l 1234 | dd of=/dev/null'

The way this works is that the client connects to port 1234 on the local machine, the connection is forwarded over SSH to port 1234 on some.remote.host where ncat reads the input, writes the output over a pipe to dd which, in turn, writes the output to /dev/null. I use dd to give us some information about how much data was received when the connection closes. Using a distant some.remote.host will help illustrate the blocking behaviour because data clearly can’t be transferred as quickly as the client can copy it into the kernel.

這種方式完成這個工作是通過可獲段連接到本地主機1234端口,連接通過1234端口繼續連接到some.remote.host,用ncat讀取輸入,寫入到通過一個管道輸出dd,接下來,寫輸出數據到/dev/null。我使用dd給我們提供一些信息,關於當連接關閉時接收了多少數據。使用獨立some.remote.host可以幫助我們展示阻塞行爲,因爲數據不能傳送的足夠快,當客戶單端可以拷貝數據到內核。



Blocking I/O  阻塞模式IO


To start with, let’s look at the example of using straightforward blocking I/O:

首先,我們看一個直接的阻塞式IO:

import socket

sock = socket.socket()
sock.connect(('localhost', 1234))
sock.send('foo\n' * 10 * 1024 * 1024)

This is really nice and straightforward, but the point is that this process will spend a tonne of time sleeping while the send() method completes transferring all of the data.

這個示例簡潔而直觀,但是在執行send()方法完成所有數據傳輸時,會消耗很多的時間用在等待中。





Non-Blocking I/O  非阻塞模式IO


In order to avoid this blocking behaviour, we can set the socket to non-blocking and use select() to find out when the socket is writable:

爲了防止阻塞行爲,我們可以設置端口爲非阻塞並且使用select()來查詢端口是否可以寫入:


import errno
import select
import socket

sock = socket.socket()
sock.connect(('localhost', 1234))
sock.setblocking(0)

buf = buffer('foo\n' * 10 * 1024 * 1024)
print "starting"
while len(buf):
    try:
        buf = buf[sock.send(buf):]
    except socket.error, e:
        if e.errno != errno.EAGAIN:
            raise e
        print "blocking with", len(buf), "remaining"
        select.select([], [sock], [])
        print "unblocked"
print "finished"

As you can see, when send() returns an EAGAIN error, we call select() and will sleep until the socket is writable. This is a basic example of an event loop. It’s obviously a loop, but the “event” part refers to our waiting on the “socket is writable” event.

如你所見,當send()返回一個EAGAIN 錯誤,我們可以調用select()並且等待端口變爲可寫。這個是一個基本的事件循環。是一個很明顯的循環,但是“事件”部分指示出了等待“端口變爲可用”的事件。


This example doesn’t look terribly useful because we’re still spending the same amount of time sleeping but we could in fact be doing useful rather than sleeping in select(). For example, if we had a listening socket, we could also pass it to select() and select() would tell us when a new connection is available. That way we could easily alternate between handling new connections and writing data to our socket.

這個例子貌似不怎麼有效,因爲我們依然還是耗費了同樣的時間用在等待上,但是我們可以做有用的事情,而不是讓它在等待時候休眠。例如,當我們監聽端口時,可以使用select(),select()可以告訴我們一個新的連接可用。這樣我們更容易切換新的連接並且寫入數據到端口。



To prove this “do something useful while we’re waiting” idea, how about we add a little busy loop to the I/O loop:

下面的方法實現,“做一些有用的事情”在我們等待的時候,我們怎樣可以添加一個“小忙”的循環在外層IO的內部。


        if e.errno != errno.EAGAIN:
            raise e

        i = 0
        while i < 5000000:
            i += 1

        print "blocking with", len(buf), "remaining"
        select.select([], [sock], [], 0)
        print "unblocked"

The difference is we’ve passed a timeout of zero to select() – this means select() never actually block – and any time send() would have blocked, we do a bunch of computation in user-space. If we run this using the ‘time’ command you’ll see something like:

這裏的區別是設置一個超時時間爲零(這意味着select()永遠不會真正阻塞),執行send()前有一個(服務被)阻塞等待時間,我們可以在空閒時間執行用戶功能。如果我們運行這個示例,使用“time”命令你將可以看到如下結果:


$> time python ./test-nonblocking-write.py 
starting
blocking with 8028160 remaining
unblocked
blocking with 5259264 remaining
unblocked
blocking with 4456448 remaining
unblocked
blocking with 3915776 remaining
unblocked
blocking with 3768320 remaining
unblocked
blocking with 3768320 remaining
unblocked
blocking with 3670016 remaining
unblocked
blocking with 3670016 remaining
...
real    0m10.901s
user    0m10.465s
sys     0m0.016s

The fact that there’s very little difference between the ‘real’ and ‘user’ times means we spent very little time sleeping. We can also see that sometimes we get to run the busy loop multiple times while waiting for the socket to become writable.

事實上這裏有一些些許的不同在real和user時間之間,也就是我們花費了很少的時間在休眠狀態。我們可以看到有些時候,我們獲得了在等待socket變爲可用前,多次運行(間歇地)持續循環。




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