Python 异步IO技术
异步(asynchronous)编程模式是相对于同步方式的另一种编程思路。 C10K的问题提出后, 各种编程语言都出现了解决高并发的技术栈, 而早在Python2时期, Twisted、Tornado和Gevent这三个库用不同的技术路径解决了高并发。[1] 其中就有用到Python的一些异步实现方法。 此文将对异步编程的基本思想和Python的实现方式进行阐述。
文章目录
- 概念解释,同步\异步\阻塞\非阻塞\协程
- 高并发问题解决方案:多线程与异步IO
- asyncio解决的问题
- Python异步IO生态圈
- 并发哪家强?应用:异步爬虫测试
概念解释,同步\异步\阻塞\非阻塞\协程
同步异步(Synchornous\Asynchronous)
这一概念是针对主程序来说的, 如果主程序遇到阻塞的任务时选择等待,那么这种行为就是同步的, 如果不等待选择执行其他的任务,就是异步的;
阻塞非阻塞(Blocking\Unblocking)
这一概念是针对任务来说的, 如果某项任务在执行费时IO操作时不能挂起跳出(不能把执行权归还主程序)那么此项任务就是阻塞的, 相反就是非阻塞的。
其实不必将上面四个概念区分的很清楚,只需明白异步非阻塞编程的基本思路是将IO密集型的任务时间节省出来让CPU尽可能多地去完成计算密集型任务;
协程(coroutines)
Python最早是用生成器写的协程,3.4后可以用asyncio.coroutine装饰一个协程,3.5加入了新的关键字async\await协程成为新的语法。3.6还引入了异步生成器。
协程的定义由很多,我比较认同可以挂起中断的函数 这一说法,前面概念中讲到非阻塞的任务是可以挂起中断的,那么协程正可以实现此项任务;
协程包含两种情况:
- 协程函数:async def 或者 @asyncio.coroutine
- 协程函数所返回的对象
协程的运作方式:
- 通过result = await future或者 result = yeild from future,悬挂协程,直到future完成,获取future的结果/异常
- 通过 result = await coroutine 或者 result = yeild from coroutine 等待另一个协程的结果(或者异常,异常会被传播)。
- returen expression 返回该协程的结果,被await,或者yield from获取。
- raise exception,抛出异常,被await,或者yield from获取。
协程的典型应用案例,摘自《Fluent Python》
# 计算移动平均值
from collections import namedtuple
Result = namedtuple('Result', 'count average')
# 子生成器
def averager():
total = 0.0
count = 0
average = None
while True:
term = yield
if term is None:
break
total += term
count += 1
average = total/count
return Result(count, average)
# 委派生成器
def grouper(results, key):
while True:
results[key] = yield from averager()
# 客户端代码,即调用方
def main(data):
results = {}
for key, values in data.items():
group = grouper(results, key)
# 协程需要激活
next(group)
for value in values:
# 传递值给自生成器
group.send(value)
group.send(None) # 重要!
report(results)
# 输出报告
def report(results):
for key, result in sorted(results.items()):
group, unit = key.split(';')
print('{:2} {:5} averaging {:.2f}{}'.format(
result.count, group, result.average, unit))
data = {
'girls;kg':
[40.9, 38.5, 44.3, 42.2, 45.2, 41.7, 44.5, 38.0, 40.6, 44.5],
'girls;m':
[1.6, 1.51, 1.4, 1.3, 1.41, 1.39, 1.33, 1.46, 1.45, 1.43],
'boys;kg':
[39.0, 40.8, 43.2, 40.8, 43.1, 38.6, 41.4, 40.6, 36.3],
'boys;m':
[1.38, 1.5, 1.32, 1.25, 1.37, 1.48, 1.25, 1.49, 1.46],
}
if __name__ == '__main__':
main(data)
调用协程函数并不能使该协程运行。调用协程函数所返回的协程对象,在被你安排执行之前,不会做任何事情。有两种方式可以启动它:[2]
- 通过在一个已经启动的协程中调用:await coroutine或者yield from coroutine
- 或者通过ensure_task()以及loop.create_task()安排协程的执行。(使用asyncio库)
高并发问题解决方案:多线程与异步IO
其实解决高并发就是实现的程序的异步和非阻塞,而多线程\多进程是大多数比较熟悉的方案,将阻塞任务挂起到多个线程, 由操作系统实现线程\进程间的调度,concurrent.futures对象的出现使得多线程的非阻塞任务可以使用回调函数, 参考下例:
from concurrent.futures import ThreadPoolExecutor, as_completed, wait, FIRST_COMPLETED
from concurrent.futures import Future
from multiprocessing import Pool
from functools import partial
import time
def get_html(times):
t