python异步编程之asyncio(百万并发)

前言: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():
await asyncio.sleep(
1)
print('Hello World:%s' % time.time())

if name =='main':
loop
= asyncio.get_event_loop()
tasks
= [hello() for i in range(5)]
loop.run_until_complete(asyncio.wait(tasks))

 输出:

Hello World:1527595104.8338501
Hello World:1527595104.8338501
Hello World:1527595104.8338501
Hello World:1527595104.8338501
Hello World:1527595104.8338501

 

async def 用来定义异步函数,await 表示当前协程任务等待睡眠时间,允许其他任务运行。然后获得一个事件循环,主线程调用 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......

 

异常解决

假如你的并发达到 2000 个,程序会报错:ValueError: too many file descriptors in select()。报错的原因字面上看是 Python 调取的 select 对打开的文件有最大数量的限制,这个其实是操作系统的限制,linux 打开文件的最大数默认是 1024,windows 默认是 509,超过了这个值,程序就开始报错。这里我们有三种方法解决这个问题:

1. 限制并发数量。(一次不要塞那么多任务,或者限制最大并发数量)

2. 使用回调的方式

3. 修改操作系统打开文件数的最大限制,在系统里有个配置文件可以修改默认值,具体步骤不再说明了。

不修改系统默认配置的话,个人推荐限制并发数的方法,设置并发数为 500,处理速度更快。

#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()