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变为可用前,多次运行(间歇地)持续循环。




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