起因 #
异步的出现主要是单线程的io等待,由于任务大部分是io处于等待,假如让一个线程工作,所有任务按照流水线形式执行,假如一个请求需要1秒,五个请求需要五秒,那么如果能让他们同时运行的话,那么速度就能增加五倍
如何让五个任务同时进行有两种方法
- 多线程
- 异步
调试过过多线程的人都知道,线程就是从头到复制主线程一遍,开多个线程不仅成本高,而且调试成本高,异步就不一样呢,你可以把它当做一个单线程来进行编程,而且比多线程更加高效
Python异步的多种实现 #
Python实现异步的框架有很多,但是核心思想大概是基于下面两种方式
- twister
- gevent
twister思想是将异步操作封装起来,通过回调的方式来操作,我们看scrapy
里面中间请求的实现就是twister
方式
scrapy.Request(url='xxx', callback=func)
通过传递封装的request
,当框架帮我们请求完后,会通过callback进行回调,如果你的请求很简单那还好,只需要回调一次就可以,假如你的请求较复杂,那么你就会进入回调地狱(callback hell)
而且你还要写处理各种回调产生的异常,你可以看看scrapy
中间件的实现就知道scrapy
的异常处理有多繁琐了。但是中间间的存在的确让我们代码模块话更加容易,这里暂且不谈。
twister
这种回调比较反人类,它必须依赖背后的核心进行调度,离开了背后核心的支持,这个根本跑不起来,而且由于它依赖回调来进行后续步骤处理,所以我们的代码必须被切分为不同的部分,假如我们不知道背后的核心如何回调函数或者约束,我们根本不知道这两个函数是有关联的
这种编程方式比较有利于模块话开发,但是对于我们熟悉顺序编程来看,这种回调方式显然是一场噩梦,相比于twister
这种回调方式,gevent
采用的是绿色协程的方式进行回调。
PEP-380定义了yield from
的语句,Python3.3开始使用,为了区别协程和生成器,Python3.5开始使用await
代替yield from
,这样协程就有了一个专门的方法来声明(await
和async
),后者用来标记异步函数
协程之所以能够在异步中大方光彩,其中很大一部分就是协程天生就是异步的,理解协程我们可以从一个简单的生成器与普通函数来对比
a = (x for x in range(10))
b = [x for x in range(10))
我们来看这样一个生成器a,一般我们来用这个生成器必须加 for
循环才能得到里面的值,假如我们尝试使用a.send(None)
,我们会发现,我们依次从返回值得到了b里面的序列
就是这么一个send与接受的功能让我们实现了一种”绿色“回调,就是协程这个性质让他写异步变得更加顺理成章了,而且相比twister
回调,协程的回调更为彻底,它把”自己”包装起来全部回调回去了。
了解异步基础 #
前面简单的聊了协程的性质,现在谈谈异步存在的基础,异步的存在最关键的在于等待,为了了解这个等待意思和后面解读asycio
库,我们先使用selectors
(Python3对select
的封装)来做个演示
import selectors
sel = selectors.DefaultSelector()
声明一个select
对象sel,现在我们要调用这个核心函数
sel.select(10)
这个10是代表timeout
的时长,也就是最长等待时间,10秒之后我们发现,这个结果返回了一个空列表,这是显而易见的,我们并没有指明让它等待什么
selectors
这个库的功能非常好理解,类比寄信,你如果想等别人回信,假如你没有寄出去你自己的信,你一直在邮箱那等,除了等到你不想等,否则你是收不到你的回信的,所以这个库的核心在于,“寄信”(register)和等信(select),然后自己选择处理信件
import selectors
import socket
sel = selectors.DefaultSelector()
def accept(sock, mask):
conn, addr = sock.accept()
print('accepted', conn, 'from', addr)
sock = socket.socket()
sock.bind(('localhost', 8000))
sock.listen(100)
sock.setblocking(False)
sel.register(sock, selectors.EVENT_READ, accept)
while True:
events = sel.select()
for key, mask in events:
callback = key.data
callback(key.fileobj, mask)
这个程序最关键的地方在于sel.register
、sel.select
和callback
那里,前者是注册函数,后面是等待,最后就是回调
上面就是twister
式最简单的回调,你可以看到,为了得到连接sock
的连接,我们必须把处理注册到等待中去,但是这只是得到sock
连接,为了成功建立一个TCP
连接,我们还得进行三次握手,还得处理每次回调时的错误
而且你可以看到回调函数与核心驱动select.select()
耦合度非常高,我们必须完全了解系统如何回调,处理一件事被回调分割成一段一段
接下来我们来看看基于gevent
的asyncio
实现
async def wget(host):
connect = asyncio.open_connection(host, 80)
reader, writer = await connect
header = 'GET / HTTP/1.0\r\nHost: %s\r\n\r\n' % host
writer.write(header.encode('utf-8'))
await writer.drain()
while True:
line = await reader.readline()
if line == b'\r\n':
break
writer.close()
我们成功的用一个函数描绘了建立一次连接并且进行通信的过程,假如你懂一点asyncio
,你就会发现它与twister
回调的不同,使用await
关键字把函数挂起,然后等待回调,根据回调接着进行下面的操作,我们成功的用同步的语句把异步写出来,而且是使用Python的原生实现,所以当asyncio
出来的时候Guido(Python之父)是多么自豪,你可以看下面引用 Tulip: Async I/O for Python 3演讲的视频
浅析Python异步实现 #
前面我们知道了异步的基础就是等待,那么Guido是如何在协程的帮助下将异步实现出来的呢,接下来我们就简单的谈一下这个实现基础
我们先将上面twister
改成gevent
方式的
sel = selectors.DefaultSelector()
@asyncio.coroutine
def get_connection(sock):
sel.register(sock, selectors.EVENT_READ)
yield True
async def create_connection():
sock = socket.socket()
sock.bind(('localhost', 8000))
sock.listen(100)
sock.setblocking(False)
await get_connection(sock)
conn, addr = sock.accept()
print('accepted', conn, 'from', addr)
event = create_connection()
event.send(None)
events = sel.select(100)
for key, mask in events:
try:
event.send(None)
except StopIteration:
pass
我们稍稍修改一下上面的twister
函数,我们创建一个get_connection
函数把sock
绑定到我们的sel
上面,然后回调一个True
,当然这个回调没有处理异常什么的,然后我们将得到的协程向其发送一个None
让它启动,这时候你在在另外一个ipython
客户端执行
import socket
socket.socket()..connect(('localhost', 8000))
然后你就会发现在主线程里面打印出来客户端的连接信息
通过这个小例子我们知道,实现异步要解决的问题就是一个公用注册器(能够注册所以的io等待),一个容器(能够存贮所以的协程),一个核心能够一直执行等待回调和处理回调(多个协程)
深入asyncio了解Python异步 #
通过上面我们简单的知道了,如何通过协程与select
合作完成异步操作,然而我们上面写的只是最最最基本的实现,接下来我们来深入asyncio
源码了解如何让异步变得更加简单
引用 #
Python 中的异步编程:Asyncio
Tulip: Async I/O for Python 3
【译】深入理解python3.4中Asyncio库与Node.js的异步IO机制