21. 异步IO
CPU的速度远远快于磁盘、网络等IO。
同步IO:遇到IO操作,如读写文件、发送网络数据时,需要等待IO操作完成,才能继续进行下一步操作。
解决IO问题的方法:
- 多线程、多进程
- 异步IO:代码需要执行一个耗时IO操作时,它只发出指令并不等待IO结果,然后去执行其他代码,等IO返回结果时再通知CPU进行处理
异步IO模型需要一个消息循环,在消息循环中,主线程不断地重复“读取消息-处理消息”这一过程:
loop = get_event_loop()
while True:
event = loop.get_event()
process_event(event)
1. 协程
协程,又称微线程,纤程,英文名Coroutine。
子程序(或称为函数)调用是通过栈实现的,一个线程就是执行一个子程序。子程序就是协程的一种特例。
子程序调用总是一个入口,一次返回,调用顺序是明确的,而协程的调用和子程序不同。协程看上去也是子程序,但执行过程中,在子程序内部可中断,然后转而进行别的子程序,在适当的时候再返回来接着执行。
协程跟多线程比的优势:
- 协程极高的执行效率。子程序切换不是线程切换,而是由程序自身控制,没有线程切换的开销,和多线程比,线程数量越多协程的性能优势就越明显
- 不需要多线程的锁机制。因为只有一个纤程,也不存在同时写变量冲突,在协程中控制共享资源不加锁只需判断状态即可
协程是一个线程执行,利用多核CPU最简单的方法是 多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。
Python通过generator实现协程。可通过for
循环迭代,也可不断调用next()
函数获取由yield
语句返回的下一个值。但yield
不仅可返回一个值,还可接受调用者发出的参数。
生产者生产消息后,直接通过yield
跳转到消费者开始执行,待消费者执行完毕后,切换回生产者继续生产,效率极高。
def consumer():
'''generator生产者'''
r = ''
while True:
n = yield r
if not n:
return
print('[CONSUMER] Consuming %s...' % n)
r = '200 OK'
def produce(c):
c.send(None) # 启动生成器
n = 0
while n < 5:
n += 1
print('[PRODUCER] Producing %s' % n)
# 一旦产生了东西,通过c.send(n)切换到consumer()执行
r = c.send(n)
print('[PRODUCER] Consumer return: %s' % r)
# 关闭consumer
c.close()
c = consumer()
produce(c)
'''
[PRODUCER] Producing 1
[CONSUMER] Consuming 1...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 2
[CONSUMER] Consuming 2...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 3
[CONSUMER] Consuming 3...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 4
[CONSUMER] Consuming 4...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 5
[CONSUMER] Consuming 5...
[PRODUCER] Consumer return: 200 OK
'''
代码解释:
- 函数
consumer()
是一个generator
,把一个consumer
传入produce
后:- 首先调用
c.send(None)
启动生成器 - 然后,一旦产生了东西,通过
s.send(n)
切换到consumer
执行 consumer
通过yield
拿到消息处理,又通过yield
把结果传回produce
拿到consumer
处理的结果,继续生产下一条消息produce
决定不生产了,通过c.close()
关闭consumer
,整个过程结束
- 首先调用
- 整个流程无锁,由一个线程执行,
produce
和consumer
协作完成任务,所以称为"协程",而非线程的抢占式多任务
2. asyncio
asyncio
是Python3.4版本引入的标准库,直接内置了对异步IO的支持。
asyncio
的编程模型就是一个消息循环,从asyncio
模块中直接获取一个EventLoop
的引用,然后把需要执行的协程扔到EventLoop
中执行,就实现了异步IO。
用asyncio
实现Hello world
代码:
import asyncio
@asyncio.coroutine
def hello():
print("Hello, World!")
# 异步调用asyncio.sleep(1)
r = yield from asyncio.sleep(1)
print("Hello again")
# 获取EventLoop
loop = asyncio.get_event_loop()
# 执行coroutine
loop.run_until_complete(hello())
loop.close()
# 打印
Hello, World!
Hello again!
代码解释:
@asyncio.coroutine
把一个generator标记为coroutine类型,然后把这个coroutine
扔到EventLoop
中执行hello()
会首先打印出Hello, World!
yield from
语句让我们方便的调用另一个generator
。由于asyncio.sleep()
也是一个coroutine
,所以线程不会等待asyncio.sleep()
,而是直接中断并执行下一个消息循环- 当
asyncio.sleep()
返回时,线程就可以从yield from
拿到返回值(此处是None),然后接着执行下一行语句
用Task封装两个coroutine
:
import threading
import asyncio
@asyncio.coroutine
def hello():
print('Hello, World! (%s)' % threading.currentThread())
yield from asyncio.sleep(1)
print('Hello, again! (%s)' % threading.currentThread())
loop = asyncio.get_event_loop()
tasks = [hello(), hello()]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()
# 第二行和第三行中间暂停1秒
'''
Hello, World! (<_MainThread(MainThread, started 4749815296)>)
Hello, World! (<_MainThread(MainThread, started 4749815296)>)
Hello, again! (<_MainThread(MainThread, started 4749815296)>)
Hello, again! (<_MainThread(MainThread, started 4749815296)>)
'''
代码解释:
- 由打印的当前线程名称可看出:两个
coroutine
是由一个线程并发执行的 - 如果把
asyncio.sleep()
换成真正的IO操作,则多个coroutine
就可由一个线程并发执行
用asyncio
的异步网络连接来获取sina、Sohu和163的网站首页:
import asyncio
@asyncio.coroutine
def wget(host):
print('wget %s...' % host)
connect = asyncio.open_connection(host, 80)
reader, writer = yield from connect
header = 'GET / HTTP/1.0\r\nHost: %s\r\n\r\n' % host
writer.write(header.encode('utf-8'))
yield from writer.drain()
while True:
line = yield from reader.readline()
if line == b'\r\n':
break
print('%s header > %s' % (host, line.decode('utf-8').rstrip()))
# Ignore the body, close the socket
writer.close()
loop = asyncio.get_event_loop()
tasks = [wget(host) for host in ['www.sina.com.cn', 'www.sohu.com', 'www.163.com']]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()
'''
wget www.sohu.com...
wget www.163.com...
wget www.sina.com.cn...
(等待一段时间,很短很短)
(打印sina的header)
www.sina.com.cn header > HTTP/1.1 302 Moved Temporarily
www.sina.com.cn header > Server: nginx
...
(打印Sohu的header)
www.sohu.com header > Content-Type: text/html
www.sohu.com header > Content-Length: 180
...
(打印163的header)
www.163.com header > HTTP/1.1 301 Moved Permanently
www.163.com header > Date: Tue, 22 Jun 2021 15:17:27 GMT
'''
可见3个连接由一个线程通过coroutine
并发完成。
知识点:
asyncio
提供了完善的异步IO支持- 异步操作需要在
coroutine
中通过yield from
完成 - 多个
coroutine
可封装成一组Task然后并发执行
3. asyncio/await
用asyncio
提供的@asyncio.coroutine
可以把一个generator标记为coroutine类型,然后在coroutine内部用yield from
调用另一个coroutine实现异步操作。
为了简化更好的标识异步IO,从Python3.5开始引入新的语法async
和await
可以让coroutine的代码更简洁易读。
async
和await
是针对coroutine的新语法。使用该语法有两步简单替换:
- 把
@asyncio.coroutine
替换为async
- 把
yield from
替换为await
导入模块:
import asyncio
import threading
上一节代码:
@asyncio.coroutine
def hello():
print('Hello, World! (%s)' % threading.currentThread())
yield from asyncio.sleep(1)
print('Hello, World! (%s)' % threading.currentThread())
loop = asyncio.get_event_loop()
tasks = [hello(), hello()]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()
'''
Hello, World! (<_MainThread(MainThread, started 13052)>)
Hello, World! (<_MainThread(MainThread, started 13052)>)
'''
# 停顿1秒后输出下方信息
'''
Hello, World! (<_MainThread(MainThread, started 13052)>)
Hello, World! (<_MainThread(MainThread, started 13052)>)
'''
新语法:
async def hello():
print('Hello, World! (%s)' % threading.currentThread())
r = await asyncio.sleep(1)
print('Hello again! (%s)' % threading.currentThread())
# 剩下的代码不变
loop = asyncio.get_event_loop()
tasks = [hello(), hello()]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()
'''
Hello, World! (<_MainThread(MainThread, started 5300)>)
Hello, World! (<_MainThread(MainThread, started 5300)>)
'''
# 停顿1秒后打印下面信息
'''
Hello again! (<_MainThread(MainThread, started 5300)>)
Hello again! (<_MainThread(MainThread, started 5300)>)
'''
4. aiohttp
asyncio
可实现单线程并发IO操作。如果仅用在客户端,发挥的作用不大。如果把asyncio
用在服务器端,例如Web服务器,由于HTTP连接就是IO操作,因此可用单线程+coroutine
实现多用户的高并发支持。
asyncio
实现了TCP、UDP、SSL等协议,aiohttp
则是基于asyncio
实现的HTTP框架。
使用:
- 安装
aiohttp
:
pip install aiohttp
- 编写一个HTTP服务器,分别处理一下URL:
/
- 首页返回b'<h1>Index</h1>'
/hello/{name}
- 根据URL参数返回文本hello,%s!
- 代码如下:
import asyncio
from aiohttp import web
async def index(request):
await asyncio.sleep(0.5)
return web.Response(body=b'<h1>Index</h1>')
async def hello(request):
await asyncio.sleep(0.5)
text = '<h1>hello, %s!</h1>' % request.match_info['name']
return web.Response(body=text.encode('utf-8'))
async def init(loop):
app = web.Application(loop=loop)
app.router.add_route('GET', '/', index)
app.router.add_route('GET', '/hello/{name}', hello)
srv = await loop.create_server(app.make_handler(), '127.0.0.1', 8000)
print('Server started at http://127.0.0.1:8000...')
return srv
loop = asyncio.get_event_loop()
loop.run_until_complete(init(loop))
loop.run_forever()
# 试了下不行,下载了个文件什么鬼。。。可能是Windows的锅
注意aiohttp的初始化函数init()也是一个coroutine,loop.create_server()则利用asyncio创建TCP服务。