Asyncio是一个异步编程的框架,可以解决异步编程,协程调度问题,线程问题,是整个异步IO的解决方案。
在学习asyncio之前,我们先来理清楚同步/异步的概念:
同步是指完成事务的逻辑,先执行第一个事务,如果阻塞了,会一直等待,直到这个事务完成,再执行第二个事务,顺序执行 一直等待
异步是和同步相对的,异步是指在处理调用这个事务的之后,不会等待这个事务的处理结果,直接处理第二个事务去了,通过状态、通知、回调来通知调用者处理结果。
异步IO采用消息循环的模式,重复“读取消息—处理消息”的过程,也就是说异步IO模型”需要一个消息循环,在消息循环中,主线程不断地重复“读取消息-处理消息”这一过程。
其中协程编程离不开的三大要点:
以下内容有删减的摘自 :
Asyncio并发编程www.langzi.funasync def get_url_title(url):
# 使用关键词async定义一个协程
print('开始访问网站:{}'.format(url))
await asyncio.sleep(2)
# 这一步至关重要
# asyncio.sleep(2) 功能:异步非阻塞等待2s,作用是模拟访问网站消耗的时间
# await 的作用类似 yield,即这个时候把线程资源控制权交出去,监听这个描述符直到这个任务完成
# await 后面只能接三种类型
'''
1. 协程:Python 协程属于 可等待 对象,因此可以在其他协程中被等待:
2. 任务:任务 被用来设置日程以便 并发 执行协程。(当一个协程通过 asyncio.create_task() 等函数被打包为一个 任务,该协程将自动排入日程准备立即运行)
3. Future 对象:Future 是一种特殊的 低层级 可等待对象,表示一个异步操作的 最终结果。(当一个 Future 对象 被等待,这意味着协程将保持等待直到该 Future 对象在其他地方操作完毕。)
如果await time.sleep(2) 是会报错的
'''
print('网站访问成功')
if __name__ == '__main__':
start_time = time.time()
loop = asyncio.get_event_loop()
# 一行代码创造事件循环
loop.run_until_complete(get_url_title('http://www.langzi.fun'))
# 这是一个阻塞的方法,可以理解成多线程中的join方法
# 直到get_url_title('http://www.langzi.fun')完成后,才会继续执行下面的代码
end_time = time.time()
print('消耗时间:{}'.format(end_time-start_time))
返回结果:
开始访问网站:http://www.langzi.fun
网站访问成功
消耗时间:2.0018768310546875
协程的优势是多任务协作,单任务访问网站没法发挥出他的功能,一次性访问多个网站或者一次性等待多个IO响应时间才能发挥它的优势。
# -*- coding:utf-8 -*-
import asyncio
import time
async def get_url_title(url):
print('开始访问网站:{}'.format(url))
await asyncio.sleep(2)
print('网站访问成功')
if __name__ == '__main__':
start_time = time.time()
loop = asyncio.get_event_loop()
# 创造一个事件循环
tasks = [get_url_title('http://www.langzi.fun')for i in range(10)]
# 这个列表代表总任务量,即执行10次get_url_title()函数
loop.run_until_complete(asyncio.wait(tasks))
# asyncio.wait后面接上非空可迭代对象,一般来说是功能函数列表
# 功能是一次性提交多个任务,等待完成
# loop.run_until_complete(asyncio.gather(*tasks))
# 和上面代码功能一致,但是gather更加高级,如果是列表就需要加上*
# 这里会等到全部的任务执行完后才会执行后面的代码
end_time = time.time()
print('消耗时间:{}'.format(end_time-start_time))
对一个网站发起10次请求,返回结果:
开始访问网站:http://www.langzi.fun
开始访问网站:http://www.langzi.fun
开始访问网站:http://www.langzi.fun
开始访问网站:http://www.langzi.fun
开始访问网站:http://www.langzi.fun
开始访问网站:http://www.langzi.fun
开始访问网站:http://www.langzi.fun
开始访问网站:http://www.langzi.fun
开始访问网站:http://www.langzi.fun
开始访问网站:http://www.langzi.fun
网站访问成功
网站访问成功
网站访问成功
网站访问成功
网站访问成功
网站访问成功
网站访问成功
网站访问成功
网站访问成功
网站访问成功
消耗时间:2.0015649795532227
gather与wait的区别:
即gather更加高级,他可以将任务分组,也可以取消任务
import asyncio
async def get_url_title(url):
print('开始访问网站:{}'.format(url))
await asyncio.sleep(2)
print('网站访问成功')
return 'success'
if __name__ == '__main__':
loop = asyncio.get_event_loop()
# 使用wait方法
# tasks = [get_url_title('http://www.langzi.fun')for i in range(10)]
# loop.run_until_complete(asyncio.wait(tasks))
# 使用gather方法实现分组导入(方法1)
group1 = [get_url_title('http://www.langzi.fun')for i in range(3)]
group2 = [get_url_title('http://www.baidu.com')for i in range(5)]
loop.run_until_complete(asyncio.gather(*group1,*group2))
# 这种方法会把两个全部一次性导入
# 使用gather方法实现分组导入(方法2)
group1 = [get_url_title('http://www.langzi.fun')for i in range(3)]
group2 = [get_url_title('http://www.baidu.com')for i in range(5)]
group1 = asyncio.gather(*group1)
group2 = asyncio.gather(*group2)
#group2.cancel() 取消group2任务
loop.run_until_complete(asyncio.gather(group1,group2))
# 这种方法会先把group1导入,然后导入group2
返回结果:
开始访问网站:http://www.baidu.com
开始访问网站:http://www.baidu.com
开始访问网站:http://www.langzi.fun
开始访问网站:http://www.baidu.com
开始访问网站:http://www.langzi.fun
开始访问网站:http://www.langzi.fun
开始访问网站:http://www.baidu.com
开始访问网站:http://www.baidu.com
网站访问成功
网站访问成功
网站访问成功
网站访问成功
网站访问成功
网站访问成功
网站访问成功
网站访问成功
开始访问网站:http://www.langzi.fun
开始访问网站:http://www.langzi.fun
开始访问网站:http://www.langzi.fun
开始访问网站:http://www.baidu.com
开始访问网站:http://www.baidu.com
开始访问网站:http://www.baidu.com
开始访问网站:http://www.baidu.com
开始访问网站:http://www.baidu.com
网站访问成功
网站访问成功
网站访问成功
网站访问成功
网站访问成功
网站访问成功
网站访问成功
网站访问成功
另外一种使用gather获取返回结果:
import asyncio
async def get_url_title(url):
print('开始访问网站:{}'.format(url))
await asyncio.sleep(2)
print('网站访问成功')
return 'success'
if __name__ == '__main__':
loop = asyncio.get_event_loop()
# 使用gather方法传递任务获取结果
group1 = asyncio.ensure_future(get_url_title('http://www.langzi.fun'))
loop.run_until_complete(asyncio.gather(group1))
# 如果不是列表就不需要加*
print(group1.result())
返回结果:
开始访问网站:http://www.langzi.fun
网站访问成功
success
还有一些复杂的区别转移到python 异步协程中查看
协程的调用和组合十分灵活,尤其是对于结果的处理,如何返回,如何挂起,需要逐渐积累经验和前瞻的设计。
# -*- coding:utf-8 -*-
import asyncio
import time
async def get_url_title(url):
print('开始访问网站:{}'.format(url))
await asyncio.sleep(2)
print('网站访问成功')
return 'success'
if __name__ == '__main__':
start_time = time.time()
loop = asyncio.get_event_loop()
# 创建一个事件循环
get_future = loop.create_task(get_url_title('http://www.langzi.fun'))
#get_future = asyncio.ensure_future(get_url_title('http://www.langzi.fun'))
# 这两行代码功能用法一模一样
loop.run_until_complete(get_future)
print('获取结果:{}'.format(get_future.result()))
# 获取结果
end_time = time.time()
print('消耗时间:{}'.format(end_time-start_time))
返回结果:
开始访问网站:http://www.langzi.fun
网站访问成功
获取结果:success
消耗时间:2.0019724369049072
如果是多个网址传入,访问多个网址的返回值呢?只需要把前面的知识点汇总一起即可使用:
if __name__ == '__main__':
start_time = time.time()
loop = asyncio.get_event_loop()
# 创建一个事件循环
tasks = [loop.create_task(get_url_title('http://www.langzi.fun')) for i in range(10)]
# 把所有要返回的函数加载到一个列表
loop.run_until_complete(asyncio.wait(tasks))
# 这里和上面用法一样
print('获取结果:{}'.format([x.result() for x in tasks]))
# 因为结果都在一个列表,在列表中取值即可
end_time = time.time()
print('消耗时间:{}'.format(end_time-start_time))
返回结果:
开始访问网站:http://www.langzi.fun
开始访问网站:http://www.langzi.fun
开始访问网站:http://www.langzi.fun
开始访问网站:http://www.langzi.fun
开始访问网站:http://www.langzi.fun
开始访问网站:http://www.langzi.fun
开始访问网站:http://www.langzi.fun
开始访问网站:http://www.langzi.fun
开始访问网站:http://www.langzi.fun
开始访问网站:http://www.langzi.fun
网站访问成功
网站访问成功
网站访问成功
网站访问成功
网站访问成功
网站访问成功
网站访问成功
网站访问成功
网站访问成功
网站访问成功
获取结果:['success', 'success', 'success', 'success', 'success', 'success', 'success', 'success', 'success', 'success']
消耗时间:2.0016491413116455
上面的例子是一个协程函数,当这个协程函数的await xxx执行完毕后,想要执行另一个函数后,然后再返回这个协程函数的返回结果该这么做:
# -*- coding:utf-8 -*-
import asyncio
from functools import partial
# partial的功能是 固定函数参数,返回一个新的函数。你可以这么理解:
'''
from functools import partial
def go(x,y):
return x+y
g = partial(go,y=2)
print(g(1))
返回结果:3
g = partial(go,x=5,y=2)
print(g())
返回结果:7
'''
async def get_url_title(url):
print('开始访问网站:{}'.format(url))
await asyncio.sleep(2)
print('网站访问成功')
# 当这个协程函数快要结束返回值的时候,会调用下面的call_back函数
# 等待call_back函数执行完毕后,才返回这个协程函数的值
return 'success'
def call_back(future,url):
# 注意这里必须要传递future参数,因为这里的future即代表下面的get_future对象
print('检测网址:{}状态正常'.format(url))
if __name__ == '__main__':
loop = asyncio.get_event_loop()
# 创建一个事件循环
get_future = loop.create_task(get_url_title('http://www.langzi.fun'))
# 将一个任务注册到loop事件循环中
get_future.add_done_callback(partial(call_back,url = 'http://www.langzi.fun'))
# 这里是设置,当上面的任务完成要返回结果的时候,执行call_back函数
# 注意call_back函数不能加上(),也就意味着你只能依靠partial方法进行传递参数
loop.run_until_complete(get_future)
# 等待任务完成
print('获取结果:{}'.format(get_future.result()))
# 获取结果
返回结果:
开始访问网站:http://www.langzi.fun
网站访问成功
检测网址:http://www.langzi.fun状态正常
获取结果:success
存在多个任务协程,想使用ctrl c退出协程,使用例子讲解:
import asyncio
async def get_time_sleep(t):
print('开始运行,等待:{}s'.format(t))
await asyncio.sleep(t)
print('运行结束')
if __name__ == '__main__':
loop = asyncio.get_event_loop()
# 创建一个事件循环
task_1 = get_time_sleep(1)
task_2 = get_time_sleep(2)
task_3 = get_time_sleep(3)
tasks = [task_1,task_2,task_3]
# 三个协程任务加载到一个列表
try:
loop.run_until_complete(asyncio.wait(tasks))
except KeyboardInterrupt:
# 当检测到键盘输入 ctrl c的时候
all_tasks = asyncio.Task.all_tasks()
# 获取注册到loop下的所有task
for task in all_tasks:
print('开始取消协程')
task.cancel()
# 取消该协程,如果取消成功则返回True
loop.stop()
# 停止循环
loop.run_forever()
# loop事件循环一直运行
# 这两步必须要做
finally:
loop.close()
# 关闭事件循环
run_forever 会一直运行,直到 stop 被调用,但是你不能像下面这样调 stop
loop.run_forever()
loop.stop()
run_forever 不返回,stop 永远也不会被调用。所以,只能在协程中调 stop:
async def do_some_work(loop, x):
print('Waiting ' + str(x))
await asyncio.sleep(x)
print('Done')
loop.stop()
这样并非没有问题,假如有多个协程在 loop 里运行:
asyncio.ensure_future(do_some_work(loop, 1))
asyncio.ensure_future(do_some_work(loop, 3))
loop.run_forever()
第二个协程没结束,loop 就停止了——被先结束的那个协程给停掉的。
要解决这个问题,可以用 gather 把多个协程合并成一个 future,并添加回调,然后在回调里再去停止 loop。
async def do_some_work(loop, x):
print('Waiting ' + str(x))
await asyncio.sleep(x)
print('Done')
def done_callback(loop, futu):
loop.stop()
loop = asyncio.get_event_loop()
futus = asyncio.gather(do_some_work(loop, 1), do_some_work(loop, 3))
futus.add_done_callback(functools.partial(done_callback, loop))
loop.run_forever()
其实这基本上就是 run_until_complete 的实现了,run_until_complete 在内部也是调用 run_forever。
关于loop.close(),简单来说,loop 只要不关闭,就还可以再运行。
loop.run_until_complete(do_some_work(loop, 1))
loop.run_until_complete(do_some_work(loop, 3))
loop.close()
但是如果关闭了,就不能再运行了:
loop.run_until_complete(do_some_work(loop, 1))
loop.close()
loop.run_until_complete(do_some_work(loop, 3)) # 此处异常
import asyncio
async def sum_tion(x,y):
print('开始执行传入参数相加:{} + {}'.format(x,y))
await asyncio.sleep(1)
# 模拟等待1S
return (x+y)
async def print_sum(x,y):
result = await sum_tion(x,y)
print(result)
if __name__ == '__main__':
loop = asyncio.get_event_loop()
loop.run_until_complete(print_sum(1000,2000))
loop.close()
返回结果:
开始执行传入参数相加:1000 + 2000
3000
执行流程:
如果想要获取协程嵌套函数返回的值,就必须使用回调:
import asyncio
async def sum_tion(x,y)->int:
print('开始执行传入参数相加:{} + {}'.format(x,y))
await asyncio.sleep(1)
# 模拟等待1S
return (x+y)
async def print_sum(x,y):
result = await sum_tion(x,y)
return result
def callback(future):
return future.result()
if __name__ == '__main__':
loop = asyncio.get_event_loop()
future = loop.create_task(print_sum(100,200))
# 如果想要获取嵌套协程返回的值,就必须使用回调
future.add_done_callback(callback)
loop.run_until_complete(future)
print(future.result())
loop.close()
返回结果:
开始执行传入参数相加:100 + 200
300
asyncio提供定时启动协程任务,通过call_soon,call_later,call_at实现,他们的区别如下:
call_soon是立即执行
def callback(sleep_times):
print("预计消耗时间 {} s".format(sleep_times))
def stoploop(loop):
print('时间消耗完毕')
loop.stop()
if __name__ == "__main__":
start_time = time.time()
loop = asyncio.get_event_loop()
# 创建一个事件循环
loop.call_soon(callback,5)
# 立即启动callback函数
loop.call_soon(stoploop,loop)
# 上面执行完毕后,立即启动执行stoploop函数
loop.run_forever()
#要用这个run_forever运行,因为没有传入协程
print('总共耗时:{}'.format(time.time()-start_time))
返回结果:
预计消耗时间 5 s
时间消耗完毕
总共耗时:0.0010013580322265625
call_later是设置一定时间启动执行
def callback(sleep_times):
print("预计消耗时间 {} s".format(sleep_times))
def stoploop(loop):
print('时间消耗完毕')
loop.stop()
if __name__ == "__main__":
start_time = time.time()
loop = asyncio.get_event_loop()
loop.call_later(1,callback,1.0)
# 等待1秒后执行callback函数,传入参数是1.0
loop.call_later(5,stoploop,loop)
# 等待5秒后执行stoploop函数,传入参数是loop
loop.run_forever()
print('总共耗时:{}'.format(time.time()-start_time))
返回结果:
预计消耗时间 1.0 s
时间消耗完毕
总共耗时:5.002613544464111
call_at类似与call_later,但是他指定的时间不再是传统意义上的时间,而是loop的内部时钟时间,效果和call_later一样, call_later内部其实调用了call_later
import time
import asyncio
def callback(loop):
print("传入loop.time()时间为: {} s".format(loop.time()))
def stoploop(loop):
print('时间消耗完毕')
loop.stop()
if __name__ == "__main__":
start_time = time.time()
loop = asyncio.get_event_loop()
now = loop.time()
# loop内部的时钟时间
loop.call_at(now+1,callback,loop)
# 等待loop内部时钟时间加上1s后,执行callba函数,传入参数为loop
loop.call_at(now+3,callback,loop)
# 等待loop内部时钟时间加上3s后,执行callba函数,传入参数为loop
loop.call_at(now+5,stoploop,loop)
# 等待loop内部时钟时间加上1s后,执行stoploop函数,传入参数为loop
返回结果:
传入loop.time()时间为: 3989.39 s
传入loop.time()时间为: 3991.39 s
时间消耗完毕
总共耗时:5.002060174942017
call_soon_threadsafe用法和call_soon一致。但在涉及多线程时, 会使用它.
Asyncio是异步IO编程的解决方案,异步IO是包括多线程,多进程,和协程的。所以asyncio是可以完成多线程多进程和协程的,在开头说到,协程是单线程的,如果遇到阻塞的话,会阻塞所有的代码任务,所以是不能加入阻塞IO的,但是比如requests库是阻塞的,socket如果不设置setblocking(false)的话,也是阻塞的,这个时候可以放到一个线程中去做也是可以解决的,即在协程中集成阻塞IO,就加入多线程一起解决问题。
from concurrent.futures import ThreadPoolExecutor
import requests
import asyncio
import time
import re
def get_url_title(url):
# 功能是获取网址的标题
r = requests.get(url)
try:
title = re.search('(.*?) ',r.content.decode(),re.S|re.I).group(1)
except Exception as e:
title = e
print(title)
if __name__ == '__main__':
start_time = time.time()
loop = asyncio.get_event_loop()
# 创建一个事件循环
p = ThreadPoolExecutor(5)
# 创建一个线程池,开启5个线程
tasks = [loop.run_in_executor(p,get_url_title,'http://www.langzi.fun')for i in range(10)]
# 这一步很重要,使用loop.run_in_executor()函数:内部接受的是阻塞的线程池,执行的函数,传入的参数
# 即对网站访问10次,使用线程池访问
loop.run_until_complete(asyncio.wait(tasks))
# 等待所有的任务完成
print(time.time()-start_time)
返回结果:
Langzi - Never Setter 永不将就 - 致力于Python开发网络安全工具,分享Python底层与进阶知识,漏洞扫描器开发与爬虫开发
Langzi - Never Setter 永不将就 - 致力于Python开发网络安全工具,分享Python底层与进阶知识,漏洞扫描器开发与爬虫开发
Langzi - Never Setter 永不将就 - 致力于Python开发网络安全工具,分享Python底层与进阶知识,漏洞扫描器开发与爬虫开发
Langzi - Never Setter 永不将就 - 致力于Python开发网络安全工具,分享Python底层与进阶知识,漏洞扫描器开发与爬虫开发
Langzi - Never Setter 永不将就 - 致力于Python开发网络安全工具,分享Python底层与进阶知识,漏洞扫描器开发与爬虫开发
Langzi - Never Setter 永不将就 - 致力于Python开发网络安全工具,分享Python底层与进阶知识,漏洞扫描器开发与爬虫开发
Langzi - Never Setter 永不将就 - 致力于Python开发网络安全工具,分享Python底层与进阶知识,漏洞扫描器开发与爬虫开发
Langzi - Never Setter 永不将就 - 致力于Python开发网络安全工具,分享Python底层与进阶知识,漏洞扫描器开发与爬虫开发
Langzi - Never Setter 永不将就 - 致力于Python开发网络安全工具,分享Python底层与进阶知识,漏洞扫描器开发与爬虫开发
Langzi - Never Setter 永不将就 - 致力于Python开发网络安全工具,分享Python底层与进阶知识,漏洞扫描器开发与爬虫开发
5.589502334594727
访问10次消耗时间为5.5s,尝试将 p = ThreadPoolExecutor(10),线程数量设置成10个线程,消耗时间为4.6s,改用从进程池p = ProcessPoolExecutor(10),也是一样可以运行的,不过10个进程消耗时间也是5.5s,并且消耗更多的CPU资源。
import asyncio
from concurrent.futures import ThreadPoolExecutor
import socket
from urllib.parse import urlparse
import time
import re
def get_url(url):
# 通过socket请求html
url = urlparse(url)
host = url.netloc
path = url.path
if path == "":
path = '/'
# 建立socket连接
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect((host, 80))
client.send(
"GET {} HTTP/1.1rnHost:{}rnConnection:closernrn".format(path, host).encode('utf8'))
data = b""
while True:
d = client.recv(1024)
if d:
data += d
else:
break
data = data.decode('utf8')
html_data = data.split('rnrn')[1]
# 把请求头信息去掉, 只要网页内容
title = re.search('(.*?) ',html_data,re.S|re.I).group(1)
print(title)
client.close()
if __name__ == '__main__':
start_time = time.time()
loop = asyncio.get_event_loop()
p = ThreadPoolExecutor(3) # 线程池 放3个线程
tasks = [loop.run_in_executor(p,get_url,'http://www.langzi.fun') for i in range(10)]
loop.run_until_complete(asyncio.wait(tasks))
print('last time:{}'.format(time.time() - start_time))
返回结果:
Langzi - Never Setter 永不将就 - 致力于Python开发网络安全工具,分享Python底层与进阶知识,漏洞扫描器开发与爬虫开发
Langzi - Never Setter 永不将就 - 致力于Python开发网络安全工具,分享Python底层与进阶知识,漏洞扫描器开发与爬虫开发
Langzi - Never Setter 永不将就 - 致力于Python开发网络安全工具,分享Python底层与进阶知识,漏洞扫描器开发与爬虫开发
Langzi - Never Setter 永不将就 - 致力于Python开发网络安全工具,分享Python底层与进阶知识,漏洞扫描器开发与爬虫开发
Langzi - Never Setter 永不将就 - 致力于Python开发网络安全工具,分享Python底层与进阶知识,漏洞扫描器开发与爬虫开发
Langzi - Never Setter 永不将就 - 致力于Python开发网络安全工具,分享Python底层与进阶知识,漏洞扫描器开发与爬虫开发
Langzi - Never Setter 永不将就 - 致力于Python开发网络安全工具,分享Python底层与进阶知识,漏洞扫描器开发与爬虫开发
Langzi - Never Setter 永不将就 - 致力于Python开发网络安全工具,分享Python底层与进阶知识,漏洞扫描器开发与爬虫开发
Langzi - Never Setter 永不将就 - 致力于Python开发网络安全工具,分享Python底层与进阶知识,漏洞扫描器开发与爬虫开发
Langzi - Never Setter 永不将就 - 致力于Python开发网络安全工具,分享Python底层与进阶知识,漏洞扫描器开发与爬虫开发
last time:5.132313966751099
import asyncio
from urllib.parse import urlparse
import time
async def get_url(url):
# 通过socket请求html
url = urlparse(url)
host = url.netloc
path = url.path
if path == "":
path = '/'
# 建立socket连接
reader, writer = await asyncio.open_connection(host, 80) # 协程 与服务端建立连接
writer.write(
"GET {} HTTP/1.1rnHost:{}rnConnection:closernrn".format(path, host).encode('utf8'))
all_lines = []
async for raw_line in reader: # __aiter__ __anext__魔法方法
line = raw_line.decode('utf8')
all_lines.append(line)
html = 'n'.join(all_lines)
return html
async def main():
tasks = []
tasks = [asyncio.ensure_future(get_url('http://www.langzi.fun')) for i in range(10)]
for task in asyncio.as_completed(tasks): # 完成一个 print一个
result = await task
print(result)
if __name__ == '__main__':
start_time = time.time()
loop = asyncio.get_event_loop()
loop.run_until_complete(main())
print('last time:{}'.format(time.time() - start_time))
asyncio协程和之前讲解的select事件循环原理是一样的
既然异步协程和多进程对网络请求都有提升,那么为什么不把二者结合起来呢?在最新的 PyCon 2018 上,来自 Facebook 的 John Reese 介绍了 asyncio 和 multiprocessing 各自的特点,并开发了一个新的库,叫做 aiomultiprocess
这个库的安装方式是:
pip3 install aiomultiprocess
需要 Python 3.6 及更高版本才可使用。
使用这个库,我们可以将上面的例子改写如下:
import asyncio
import aiohttp
import time
from aiomultiprocess import Pool
start = time.time()
async def get(url):
session = aiohttp.ClientSession()
response = await session.get(url)
result = await response.text()
session.close()
return result
async def request():
url = 'http://127.0.0.1:5000'
urls = [url for _ in range(100)]
async with Pool() as pool:
result = await pool.map(get, urls)
return result
coroutine = request()
task = asyncio.ensure_future(coroutine)
loop = asyncio.get_event_loop()
loop.run_until_complete(task)
end = time.time()
print('Cost time:', end - start)
这样就会同时使用多进程和异步协程进行请求,但在真实情况下,我们在做爬取的时候遇到的情况千变万化,一方面我们使用异步协程来防止阻塞,另一方面我们使用 multiprocessing 来利用多核成倍加速,节省时间其实还是非常可观的。
和多线程多进程任务一样,协程也可以实现和需要进行同步与通信。
协程是单线程的,他的执行依赖于事件循环中最后的loop.run_until_complate()
import asyncio
num = 0
async def add():
global num
for i in range(10):
await asyncio.sleep(0.1)
num += i
async def desc():
global num
for i in range(10):
await asyncio.sleep(0.2)
num -= i
if __name__ == '__main__':
loop = asyncio.get_event_loop()
tasks = [add(),desc()]
loop.run_until_complete(asyncio.wait(tasks))
# 这里执行顺序是先执行add函数,然后执行desc函数
# 所以最后的结果是0
loop.close()
print(num)
返回结果:
0
这里使用一个共有变量,协程下不需要加锁。
# -*- coding:utf-8 -*-
import asyncio
import functools
def unlock(lock):
print('线程锁释放成功')
lock.release()
async def test(locker, lock):
print(f'{locker} 等待线程锁释放')
# ---------------------------------
# with await lock:
# print(f'{locker} 线程锁上锁')
# ---------------------------------
# 上面这两行代码等同于:
# ---------------------------------
# await lock.acquire()
# print(f'{locker} 线程锁上锁')
# lock.release()
# ---------------------------------
await lock.acquire()
print(f'{locker} 线程锁上锁')
lock.release()
print(f'{locker} 线程锁释放')
async def main(loop):
lock = asyncio.Lock()
await lock.acquire()
loop.call_later(0.5, functools.partial(unlock, lock))
# call_later() 表达推迟一段时间的回调, 第一个参数是以秒为单位的延迟, 第二个参数是回调函数
await asyncio.wait([test('任务 1 ', lock), test('任务 2', lock)])
if __name__ == '__main__':
loop = asyncio.get_event_loop()
loop.run_until_complete(main(loop))
loop.close()
返回结果:
任务 1 等待线程锁释放
任务 2 等待线程锁释放
线程锁释放成功
任务 1 线程锁上锁
任务 1 线程锁释放
任务 2 线程锁上锁
任务 2 线程锁释放
可以使用 Semaphore(信号量) 来控制并发访问的数量:
import asyncio
from aiohttp import ClientSession
async def fetch(sem,url):
async with sem:
# 最大访问数
async with ClientSession() as session:
async with session.get(url) as response:
status = response.status
res = await response.text()
print("{}:{} ".format(response.url, status))
return res
if __name__ == '__main__':
loop = asyncio.get_event_loop()
url = "http://www.langzi.fun"
sem = asyncio.Semaphore(1000)
# 设置最大并发数为1000
tasks = [loop.create_task(fetch(sem,url))for i in range(100)]
# 对网站访问100次
loop.run_until_complete(asyncio.wait(tasks))
import asyncio
async def consumer(cond, name, second):
# 消费者函数
await asyncio.sleep(second)
# 等待延迟
with await cond:
await cond.wait()
print('{}: 得到响应'.format(name))
async def producer(cond):
await asyncio.sleep(2)
for n in range(1, 3):
with await cond:
print('生产者 {} 号'.format(n))
cond.notify(n=n) # 挨个通知单个消费者
await asyncio.sleep(0.1)
async def producer2(cond):
await asyncio.sleep(2)
with await cond:
print('释放信号量,通知所有消费者')
cond.notify_all()
# 一次性通知全部的消费者
async def main(loop):
condition = asyncio.Condition()
# 设置信号量
task = loop.create_task(producer(condition))
# producer 和 producer2 是两个协程, 不能使用 call_later(), 需要用到 create_task() 把它们创建成一个 task
consumers = [consumer(condition, name, index) for index, name in enumerate(('c1', 'c2'))]
await asyncio.wait(consumers)
task.cancel()
print('---分割线---')
task = loop.create_task(producer2(condition))
consumers = [consumer(condition, name, index) for index, name in enumerate(('c1', 'c2'))]
await asyncio.wait(consumers)
task.cancel()
# 取消任务
if __name__ == '__main__':
loop = asyncio.get_event_loop()
loop.run_until_complete(main(loop))
loop.close()
返回结果:
生产者 1 号
c1: 得到响应
生产者 2 号
c2: 得到响应
---分割线---
释放信号量,通知所有消费者
c1: 得到响应
c2: 得到响应
与 Lock(锁) 不同的是, 事件被触发的时候, 两个消费者不用获取锁, 就要尽快地执行下去了
import asyncio
import functools
def set_event(event):
print('开始设置事件')
event.set()
async def test(name, event):
print('{} 的事件未设置'.format(name))
await event.wait()
print('{} 的事件已设置'.format(name))
async def main(loop):
event = asyncio.Event()
# 声明事件
print('事件是否设置: {}'.format(event.is_set()))
loop.call_later(0.1, functools.partial(set_event, event))
# 在0.1s后执行set_event()函数,对事件进行设置
await asyncio.wait([test('e1', event), test('e2', event)])
print('最终事件状态: {}'.format(event.is_set()))
if __name__ == '__main__':
loop = asyncio.get_event_loop()
loop.run_until_complete(main(loop))
loop.close()
返回结果:
事件是否设置: False
e1 的事件未设置
e2 的事件未设置
开始设置事件
e1 的事件已设置
e2 的事件已设置
最终事件状态: True
协程是单线程,因此使用list、dict就可以实现通信,而不会有线程安全问题,当然可以使用asyncio.Queue
from asyncio import Queue
queue = Queue(maxsize=3)
# queue的put和get需要用await
举个例子:
import asyncio
from asyncio import Queue
import random
import string
q = Queue(maxsize=100)
async def add():
while 1:
await q.put(random.choice(string.ascii_letters))
async def desc():
while 1:
res = await q.get()
print(res)
await asyncio.sleep(1)
if __name__ == '__main__':
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait([add(),desc()]))
loop.run_forever()
返回结果:
D
b
S
x
...
uvloop,这个使用库可以有效的加速asyncio,本库基于libuv,也就是nodejs用的那个库。使用它也非常方便,不过目前不支持windows
import asyncio
import uvloop
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
没错就是2行代码,就可以提速asyncio。
tokio同样可以做异步资源循环
import tokio
asyncio.set_event_loop_policy(tokio.EventLoopPolicy())
参考:
python异步编程之asyncio(百万并发) - 三只松鼠 - 博客园www.cnblogs.com Asyncio并发编程www.langzi.fun