异步IO
异步IO是指当程序需要执行一个耗时的IO操作时,它只发出IO操作指令,并不等待IO结果,然后就去执行其他代码了,当IO返回结果时,再通知CPU进行处理。
异步IO模型需要一个消息循环,在消息循环中,主线程不断地重复”读取消息-处理消息”这一过程:
1 | loop = get_event_loop() |
如果是使用同步IO模型,在”发出IO请求”到收到”IO完成”的这段时间里,主线程只能挂起,但在异步IO模型下,主线程并没有挂起,而是在消息循环中继续处理其他消息。这样,在异步IO模型下,一个线程就可以同时处理多个IO请求,并且没有切换线程的操作。对于大多数IO密集型的应用程序,使用异步IO将大大提升系统的多任务处理能力。
协程
协程,又称微线程,纤程。英文名Coroutine。
协程看上去就像子程序,但执行过程中,在子程序内部可中断,然后转而执行别的子程序,在适当的时候再返回来接着执行。
注意,在一个子程序中中断,去执行其他子程序,不是函数调用,有点类似CPU的中断。比如子程序A、B:
1 | def A(): |
假设由协程执行,在执行A的过程中,可以随时中断,去执行B,B也可能在执行过程中中断再去执行A,但是在A中是没有调用B的,结果可能是:
1
2
x
y
3
z
在Python中协程是通过generator来实现的。
下面的代码,使用协程实现生产者-消费者模型:
1 |
|
输出结果:
[PRODUCER] Producing 1...
[CONSUMER] Consuming 1...
[PRODUCER] Consuming return: 200 OK...
[PRODUCER] Producing 2...
[CONSUMER] Consuming 2...
[PRODUCER] Consuming return: 200 OK...
[PRODUCER] Producing 3...
[CONSUMER] Consuming 3...
[PRODUCER] Consuming return: 200 OK...
[PRODUCER] Producing 4...
[CONSUMER] Consuming 4...
[PRODUCER] Consuming return: 200 OK...
[PRODUCER] Producing 5...
[CONSUMER] Consuming 5...
[PRODUCER] Consuming return: 200 OK...
consumer
函数是一个generator
,把一个consumer
传入producer
后:
producer
首先调用c.send(None)
启动生成器一旦生成了东西,通过
c.send(n)
切换到consumer
执行consumer
通过yield
拿到消息,处理,又通过yield
把结果传回producer
拿到consumer
处理的结果,继续生产下一条消息producer
决定不生产了,通过c.close()
关闭consumer
,整个过程结束。
asyncio
asyncio
是Python 3.4版本引入的标准库,直接内置了对异步IO的支持。
asyncio
的编程模型就是一个消息循环。从asyncio
模块中直接获取一个EventLoop
的引用,然后把需要执行的协程扔到EventLoop
中执行,就实现了异步IO。
如下代码:
1 |
|
@asyncio.coroutine
把hello
生成器标记为coroutine
类型,用tasks
封装两个hello
协程,把这两个协程扔到EventLoop
中执行。asyncio.sleep(1)
看成是一个耗时1秒的IO操作,在此期间,主线程并未等待。程序最终输出以下结果:
Hello world! (<_MainThread(MainThread, started 5476)>)
Hello world! (<_MainThread(MainThread, started 5476)>)
(暂停约1秒)
Hello again! (<_MainThread(MainThread, started 5476)>)
Hello again! (<_MainThread(MainThread, started 5476)>)
由打印的当前线程名称可以看出,两个coroutine
是由同一个线程并发执行的。
如果把asyncio.sleep(1)
换成真正的IO操作,则多个coroutine
就可以由一个线程并发执行。
async/await
为了简化并更好地标识异步IO,从Python 3.5开始引入了新的语法async
和await
,可以让coroutine
的代码更简洁易读。
请注意,async
和await
是针对coroutine
的新语法,要使用新的语法,只需要做两步简单的替换:
- 把
@asyncio.coroutine
替换为async
- 把
yield from
替换为await
使用async/await
替换上面hello
协程:
1 | async def hello(): |
aiohttp
asyncio
实现了TCP、UDP、SSL等协议,aiohttp
则是基于asyncio
实现的HTTP框架。
安装aiohttp:
pip install aiohttp
编写一个HTTP服务器,分别处理以下URL:
/
- 首页返回b'<h1>Index</h1>'
/hello/{name}
- 根据URL参数返回文本hello, %s!
1 | import asyncio |
index
协程,当访问首页/
时被调用,返回一个<h1>Index</h1>
的html
文档。hello
协程,当访问/hello/{name}
时被调用,从request.match_info['name']
取出对应的参数,然后再返回对应的html
文档。init
协程,添加访问的url
,创建一个http
服务。
运行程序,在浏览器中输入http://127.0.0.1:8000/
,可以看到如下结果:
在浏览器中输入http://127.0.0.1:8000/hello/web
,可以看到如下结果:**
aiohttp
已经是一个Web框架了,但是对于使用者来说,还是相对比较底层,编写一个URL的处理函数需要这么几步:
编写一个协程的处理函数,如上面的
index
、hello
。从
request
中获取传入的参数,如上面的hello
函数的request.match_info['name']
。返回一个
Response
对象。
对于这些重复的工作可以由框架完成。
Web框架
get和post装饰器
在coroweb.py
模块中,定义get
、post
装饰器,把一个函数映射为一个URL处理函数:
1 |
|
@get
、或@post
装饰器添加了__method__
属性,表示http的GET
或POST
请求,__route__
属性,表示http请求的URI
资源地址,一个函数通过@get
、或@post
的装饰就附带了URL信息,例如上面的index
、hello
可以写成:
1 |
|
add_routes
例如在handlers.py
模块定义了使用@get
或@post
装饰器的URL处理函数,通过调用add_routes
函数进行注册。add_routes
函数的定义如下:
1 | def add_route(app, fn): |
在handlers.py
模块中添加如下函数,这样当调用add_routes(app, 'handlers')
就会自动注册handlers.py
模块含有@get
或@post
装饰器的URL处理函数:
1 | from coroweb import get |
在apps.py
模块中定义如下的测试代码,运行程序,在浏览器中输入http://127.0.0.1:8000/
、http://127.0.0.1:8000/hello/web
可以看到和上面一样的效果。
1 | async def init(loop): |
RequestHandler
通过上面的@get
和@post
装饰器,我们就可以把一个URL请求和对应的处理函数关联起来了。但是我们在处理函数中还是要从request
获取参数,进行分析,对于这个步骤,可以使用下面的RequestHandler
类来实现。
RequestHandler
定义如下:
1 | def get_required_kw_args(fn): |
上面的RequestHandler
实现了__call__
函数,所以可以想使用函数那样调用,无需创建实例后调用。
RequestHandler
从URL函数中分析其需要接收的参数,从request
中获取必要的参数,调用URL函数,有了RequestHandler
定义,我们就可以在handlers.py
的模块中使用各种参数的URL,例如:
1 |
|
将上面的add_route
函数中下面的语句:
app.router.add_route(method, path, fn)
修改为:
app.router.add_route(method, path, RequestHandler(app, fn))
把handlers.py
定义的index
、hello
函数修改为下面的定义:
1 |
|
运行apps.py
模块中的测试程序,在浏览器中输入http://127.0.0.1:8000/
、http://127.0.0.1:8000/hello/web
可以看到和上面一样的效果。
middleware
middleware
是一种拦截器,一个URL在被某个函数处理前,可以改变URL的输入、输出。middleware
的用处就在于把通用的功能从每个URL处理函数中拿出来,集中放到一个地方。例如,一个记录URL日志的logger
可以简单定义如下:
在app.py
中定义如下的logger_factory
函数:
1 | async def logger_factory(app, handler): |
对于上面的index
、hello
的URL处理函数返回的web.Response
对象,我们可以定义一个response_factory
拦截器进行处理:
在app.py
中定义如下的response_factory
函数:
1 |
|
response_factory
函数,在调用URL处理函数后,对其返回值的类型,进行判断,并返回相应的web.Response
的对象,如返回的类型是str
,对应的是html
文档,返回的类型是dict
,对应的是json
对象。
有了上面的response_factory
拦截器,handlers.py
的index
、hello
的URL处理函数,可以修改为如下:
1 |
|
在app.py
的init
函数中添加logger_factory
、response_factory
拦截器,代码如下:
1 | async def init(loop): |
运行apps.py
模块中的测试程序,在浏览器中输入http://127.0.0.1:8000/
、http://127.0.0.1:8000/hello/web
可以看到和上面一样的效果。
使用Web框架和ORM框架
有了上面的Web框架,对于使用者来说,就非常方便了。如果要处理一个URL,只需要在handlers.py
模块中添加相应的处理函数即可。
现在我们可以上文中的ORM框架结合起来使用,例如,下面的get_users
函数用于访问数据库中user
表的数据。
在handlers.py
模块中定义get_users
函数:
1 |
|
在app.py
的init
函数中,创建数据库的连接池:
1 | async def init(loop): |
运行app.py
测试程序,在浏览器中输入http://127.0.0.1:8000/users
,可以看到数据库中user
表的数据,以json
形式返回: