从Twisted谈起异步处理

来源:互联网 发布:深圳软件大厦邮编 编辑:程序博客网 时间:2024/04/28 04:02

Twisted is event-based, asynchronous framework, Twisted是基于事件的, 异步处理平台. 所以我们想要很好的理解Twisted, 就先来看看什么是异步处理?

Why Asynchronous?

There are only two ways to have a program on a single processor do 'more than one thing at a time'. Multi-threaded programming is the simplest and most popular way to do it, but there is another very different technique, that lets you have nearly all the advantages of multi-threading, without actually using multiple threads. It's really only practical if your program is I/O bound (I/O is the principle bottleneck). If your program is CPU bound, then pre-emptive scheduled threads are probably what you really need. Network servers are rarely CPU-bound, however.
(http://www.nightmare.com/medusa/programming.html)
这段讲的非常精辟, 如果你要同时做多件事, 只有两种方式: 多线程和异步. 对于I/O bound 的情况异步比较合适, 而对于CPU bound的情况多线程比较合适.

多线程
典型的场景是, 你要同时处理两件事, 你可以把一件事处理完后, 再去做第二件事, 这个就是传统的处理方式.
如果你想看到两件事同时进行, 这个用于服务器就是不要让每个用户等的太久, 每个用户都是可以看到他的job在进行中的, 这就要用到多线程调度.
相当于你把每件事分成许多小份job, 然后CPU按照某种策略进行调度, 保证每件事的job都会得到执行的机会.
这个对于单核CPU而言, 其实并没有加快做事的速度, 反而是减慢的, 因为调度本身是有耗费的. 而多线程只是改变了执行的顺序.

异步处理
典型的场景是, 打个简单的比方, 去银行取钱, 你要排队, 阻塞的方式就是, 你直接去排, 一直到轮到你.
异步的方式, 非阻塞的方式, 你取个号告诉银行你要取钱, 然后去该干吗干吗, 当到你的号了银行把你的钱取出来放到个保险柜里(队列), 你有空过来直接可以取钱走人. 异步方式的好处是显而易见的.
这儿有几个问题,
1. 你不知道什么时候钱ready了, 你可以去拿钱
简单的方式是你有空就去看看, 好了就拿, 没好就等下次(busy wait). 更好的是, 有人专门帮你看着, 发现钱ready了, 发消息(事件)给你, 你再去拿.
这个对于实际程序就是, 对于任何I/O你都会得到一个fid(都当作是文件), 你可以在程序中sleep然后一个个去check这些fid看看哪些有数据了, 就处理, 没数据的不管.
busy wait不太好,会增加无谓的CPU耗费, 比较好的方式是用SystemCall, Select, 他会去同时监控一组I/O(fid), 并把有数据的fid返回. 

2. 如果银行不支持这种模式, 你还得老老实实去排队
对于银行现在是不支持这种异步处理的, 要取钱你必须排队等. 这就是说明异步处理需要, I/O设备支持, 如果该设备不支持的话, 就没有办法进行异步处理. 现在大部分设备都是支持异步处理的.

对于这种场景, 如果用多线程是否可以了, 用也是可以用的, 这个用上面的例子就是你必须是孙悟空, 一个分身帮你排队, 然后你自己干别的去了.
但这样用不太好, 你开多个线程去等待多个I/O设备, 浪费了很多系统开销去调度线程和分配线程所需内存. 而用异步模式, 只需要一个线程就可以搞定了. 

Select函数实现原理
Here's what select() does: you pass in a set of file descriptors, in effect asking the operating system, "let me know when anything happens to any of these descriptors". (A descriptor is simply a numeric handle used by the operating system to keep track of a file, socket, pipe, or other I/O object. It is usually an index into a system table of some kind). You can also use a timeout, so that if nothing happens in the allotted period, select() will return control to your program.
select() takes three fd_set arguments; one for each of the following possible states/events: readability, writability, and exceptional conditions. The last set is less useful than it sounds.

The polling loop
Now that you know what select() does, you're ready for the final piece of the puzzle: the main polling loop. This is nothing more than a simple while loop that continually calls select() with a timeout (I usually use a 30-second timeout). Such a program will use virtually no CPU if your server is idle; it spends most of its time letting the operating system do the waiting for it. This is much more efficient than a busy-wait loop.
Here is a pseudo-code example of a polling loop:

while (any_descriptors_left):
    events = select (descriptors, timeout)
    for event in events:
        handle_event (event)


关于Select的原理, 我就不研究了, 网上有人分析过
http://blogold.chinaunix.net/u3/94284/showart_1917293.html Select函数实现原理
http://www.cppblog.com/Michael-Yolanda/articles/137771.html poll, select & epoll 原理比较分析
poll 和select应该被归类为这样的系统调用,它们可以阻塞地同时探测一组支持非阻塞的IO设备,是否有事件发生(如可读,可写,有高优先级的错误输出,出现错误等等),直至某一个设备触发了事件或者超过了指定的等待时间——也就是它们的职责不是做IO,而是帮助调用者寻找当前就绪的设备。

在Linux里面,设备都被抽象为文件,一系列的设备文件就有自己独立的虚拟文件系统,所以,设备在系统调用参数中的表示就是file description。fd其实就是一个整数(特别地,标准输入,输出,错误输出分别对应的fd是0,1,2)。
所以通过fd访问到I/O设备, 并调用其fileOperator,这里面我们要关心的一个fileOperator就是poll。因为系统调用poll和 select,就是依靠这个文件操作poll实现的。
poll函数提供信息判断当前设备是否有资源可用(如可读或写). 有两个参数,一个是文件本身,另一个可以看做是当设备尚未就绪时调用的回调函数,这个函数是把设备自己的等待队列传给内核,让内核把当前的进程挂载到其中(因为当设备就绪时,设备就应该去唤醒在自己特有等待队列中的所有节点,这样当前进程就获取了完成的信号了)。poll文件操作返回的必须是一组标准的掩码,其中的各个位指示当前的不同的就绪状态(全0为没有任何事件触发)。
Select为系统函数, 其运行在核心态, 直到其中一个设备有事件触发, 系统调用返回,回到用户态,用户就可以对相关的fd作进一步的读或者写操作了。

select的睡眠过程
支持阻塞操作的设备驱动通常会实现一组自身的等待队列如读/写等待队列用于支持上层(用户层)所需的BLOCK或NONBLOCK操作。当应用程序通过设备驱动访问该设备时(默认为BLOCK操作),若该设备当前没有数据可读或写,则将该用户进程插入到该设备驱动对应的读/写等待队列让其睡眠一段时间,等到有数据可读/写时再将该进程唤醒。
select就是巧妙的利用等待队列机制让用户进程适当在没有资源可读/写时睡眠,有资源可读/写时唤醒。
select会循环遍历它所监测的fd_set(一组文件描述符(fd)的集合)内的所有文件描述符对应的驱动程序的poll函数。驱动程序提供的poll函数首先会将调用select的用户进程插入到该设备驱动对应资源的等待队列(如读/写等待队列),然后返回一个bitmask告诉select当前资源哪些可用。当select循环遍历完所有fd_set内指定的文件描述符对应的poll函数后,如果没有一个资源可用(即没有一个文件可供操作),则select让该进程睡眠,一直等到有资源可用为止,进程被唤醒(或者timeout)继续往下执行。
其中最关键的是select调用schedule_timeout(__timeout)让该进程空出CPU, 不然就是block方式了.

select的唤醒过程
唤醒该进程的过程通常是在所监测文件的设备驱动内实现的,驱动程序维护了针对自身资源读写的等待队列。当设备驱动发现自身资源变为可读写并且有进程睡眠在该资源的等待队列上时,就会唤醒这个资源等待队列上的进程。

Twisted
其实你不用关心上面的技术, 也不用直接使用select或是poll, 因为有python, 有Twisted

原创粉丝点击