前言: python 由于 GIL(全局锁)的存在, 不能发挥多核的优势, 其性能一直饱受诟病. 然而在 IO 密集型的网络编程里, 异步处理比同步处理能提升成百上千倍的效率, 弥补了 python 性能方面的短板, 如最新的微服务框架 japronto,resquests per second 可达百万级.
python 还有一个优势是库 (第三方库) 极为丰富, 运用十分方便. asyncio 是 python3.4 版本引入到标准库, python2x 没有加这个库, 毕竟 python3x 才是未来啊, 哈哈! python3.5 又加入了 async/await 特性.
在学习 asyncio 之前, 我们先来理清楚同步 / 异步的概念:
同步是指完成事务的逻辑, 先执行第一个事务, 如果阻塞了, 会一直等待, 直到这个事务完成, 再执行第二个事务, 顺序执行...
异步是和同步相对的, 异步是指在处理调用这个事务的之后, 不会等待这个事务的处理结果, 直接处理第二个事务去了, 通过状态, 通知, 回调来通知调用者处理结果.
一, asyncio
下面通过举例来对比同步代码和异步代码编写方面的差异, 其次看下两者性能上的差距, 我们使用 sleep(1)模拟耗时 1 秒的 io 操作.
同步代码:
- import time
- def hello():
- time.sleep(1)
- def run():
- for i in range(5):
- hello()
- print('Hello World:%s' % time.time()) # 任何伟大的代码都是从 Hello World 开始的!
- if __name__ == '__main__':
- run()
输出:(间隔差不多是 1s)
- Hello World:1527595175.4728756
- Hello World:1527595176.473001
- Hello World:1527595177.473494
- Hello World:1527595178.4739306
- Hello World:1527595179.474482
异步代码:
- import time
- import asyncio
- # 定义异步函数
- async def hello():
- asyncio.sleep(1)
- print('Hello World:%s' % time.time())
- def run():
- for i in range(5):
- loop.run_until_complete(hello())
- loop = asyncio.get_event_loop()
- if __name__ =='__main__':
- run()
输出:
- Hello World:1527595104.8338501
- Hello World:1527595104.8338501
- Hello World:1527595104.8338501
- Hello World:1527595104.8338501
- Hello World:1527595104.8338501
async def 用来定义异步函数, 其内部有异步操作. 每个线程有一个事件循环, 主线程调用 asyncio.get_event_loop()时会创建事件循环, 你需要把异步的任务丢给这个循环的 run_until_complete()方法, 事件循环会安排协同程序的执行.
二, aiohttp
如果需要并发 http 请求怎么办呢, 通常是用 requests, 但 requests 是同步的库, 如果想异步的话需要引入 aiohttp. 这里引入一个类, from aiohttp import ClientSession, 首先要建立一个 session 对象, 然后用 session 对象去打开网页. session 可以进行多项操作, 比如 post, get, put, head 等.
基本用法:
- async with ClientSession() as session:
- async with session.get(url) as response:
aiohttp 异步实现的例子:
- import asyncio
- from aiohttp import ClientSession
- tasks = []
- url = "https://www.baidu.com/{}"
- async def hello(url):
- async with ClientSession() as session:
- async with session.get(url) as response:
- response = await response.read()
- print(response)
- if __name__ == '__main__':
- loop = asyncio.get_event_loop()
- loop.run_until_complete(hello(url))
首先 async def 关键字定义了这是个异步函数, await 关键字加在需要等待的操作前面, response.read()等待 request 响应, 是个耗 IO 操作. 然后使用 ClientSession 类发起 http 请求.
多链接异步访问
如果我们需要请求多个 URL 该怎么办呢, 同步的做法访问多个 URL 只需要加个 for 循环就可以了. 但异步的实现方式并没那么容易, 在之前的基础上需要将 hello()包装在 asyncio 的 Future 对象中, 然后将 Future 对象列表作为任务传递给事件循环.
- import time
- import asyncio
- from aiohttp import ClientSession
- tasks = []
- url = "https://www.baidu.com/{}"
- async def hello(url):
- async with ClientSession() as session:
- async with session.get(url) as response:
- response = await response.read()
- # print(response)
- print('Hello World:%s' % time.time())
- def run():
- for i in range(5):
- task = asyncio.ensure_future(hello(url.format(i)))
- tasks.append(task)
- if __name__ == '__main__':
- loop = asyncio.get_event_loop()
- run()
- loop.run_until_complete(asyncio.wait(tasks))
输出:
- Hello World:1527754874.8915546
- Hello World:1527754874.899039
- Hello World:1527754874.90004
- Hello World:1527754874.9095392
- Hello World:1527754874.9190395
收集 http 响应
好了, 上面介绍了访问不同链接的异步实现方式, 但是我们只是发出了请求, 如果要把响应一一收集到一个列表中, 最后保存到本地或者打印出来要怎么实现呢, 可通过 asyncio.gather(*tasks)将响应全部收集起来, 具体通过下面实例来演示.
- import time
- import asyncio
- from aiohttp import ClientSession
- tasks = []
- url = "https://www.baidu.com/{}"
- async def hello(url):
- async with ClientSession() as session:
- async with session.get(url) as response:
- # print(response)
- print('Hello World:%s' % time.time())
- return await response.read()
- def run():
- for i in range(5):
- task = asyncio.ensure_future(hello(url.format(i)))
- tasks.append(task)
- result = loop.run_until_complete(asyncio.gather(*tasks))
- print(result)
- if __name__ == '__main__':
- loop = asyncio.get_event_loop()
- run()
输出:
- Hello World:1527765369.0785167
- Hello World:1527765369.0845182
- Hello World:1527765369.0910277
- Hello World:1527765369.0920424
- Hello World:1527765369.097017
- [b'<!DOCTYPE html>\r\n<!--STATUS OK-->\r\n<html>\r\n<head>\r\n......
异常解决
假如你的并发达到 1000 个, 程序会报错: ValueError: too many file descriptors in select(). 这个报错的原因是因为 Python 调取的 select 对打开的文件字符有最大长度限制. 这里我们有两种方法解决这个问题: 1. 我们可以需要限制并发数量. 一次不要塞那么多任务, 或者限制最大并发数量. 2. 我们可以使用回调的方式. 这里个人推荐限制并发数的方法, 设置并发数为 500 或者 600, 处理速度更快.
- #coding:utf-8
- import time,asyncio,aiohttp
- url = 'https://www.baidu.com/'
- async def hello(url,semaphore):
- async with semaphore:
- async with aiohttp.ClientSession() as session:
- async with session.get(url) as response:
- return await response.read()
- async def run():
- semaphore = asyncio.Semaphore(500) # 限制并发量为 500
- to_get = [hello(url.format(),semaphore) for _ in range(1000)] #总共 1000 任务
- await asyncio.wait(to_get)
- if __name__ == '__main__':
- # now=lambda :time.time()
- loop = asyncio.get_event_loop()
- loop.run_until_complete(run())
- loop.close()
来源: https://www.cnblogs.com/shenh/p/9090586.html