0.说明
近期工作里有需求涉及到使用多线程、多进程、协程等方式对下载进行加速,但是一直以来对协程都非常头疼,完全不知所云,因此整理一个自己看得懂、拿到就能用的《5分钟上手asyncio学习笔记》。肯定理解是不到位的,但是够我现在用就行。
1.协程函数
1.1.协程与普通函数异同
首先一个普通函数大概是长这样:
def foo(*args, **kwargs):
print('hello world!')
我们称foo
为一个函数对象,foo(1, 2, kw_1=1)
则是调用函数
协程函数长这样:
async def foo(*args, **kwargs):
print('hello world!')
同样我们称foo
为一个函数对象,而foo(1, 2, kw_1=1)
不再是调用函数,而是调用函数之前的状态,即入参都已经输入好、固定住了,但是还没被执行的函数对象。
打个比方就是普通函数是一把枪,foo
就是一把枪,foo()
就是’用枪’这个操作;协程函数,foo
还是一把枪,foo()
就是’用枪的计划’,执行用枪这个操作还需要额外的步骤。
协程函数具体调用方式为:
import asyncio
async def foo(*args, **kwargs):
print('hello world!')
async_task_to_be_execute = foo('')
# 方法1: 适用于python3.7+
asyncio.run(async_task_to_be_execute)
# 方法2:
loop = asyncio.get_event_loop()
loop.run_until_complete(async_task_to_be_execute)
loop.close()
1.2.await关键字
首先我们知道协程是同一个线程里切换不同任务。举个例来理解,假如你要下载2个视频,但是只能一个一个手动操作,而你还要去洗澡。
单线程日程:下载视频1,下载完视频1去洗澡,洗完澡下载视频2。
协程日程:下载视频1,同时去洗澡,洗到一半的时候视频1下载完了,跑出去下载视频2。
例子很抽象,但是大概意思就是协程可以在执行费时间(IO密集型)的任务时进行切换,以节省时间。await
关键字在协程代码里就是告诉程序这个操作会很久,你可以先干别的。
代码示例:
import asyncio
import random
async def foo(i):
print(f'[路人{i}看到王总十分震惊]')
sleep_time = random.randint(0, 5)
await asyncio.sleep(sleep_time)
print(f'[路人{i}在{sleep_time}秒后回过神后忙道]:王总好')
async def main():
tasks = [foo(i) for i in range(1, 10)]
await asyncio.wait(tasks)
# await asyncio.gather(*tasks)
if __name__ == '__main__':
asyncio.run(main())
1.3.asyncio.wait与asyncio.gather
当我们有多个协程任务需要同时执行时就可以用这两个方法,最大差别是:前者无序,后者有序。
asyncio.wait
不关注tasks
的顺序,所以在执行2.2.中向王总打招呼的代码时路人的发言顺序是乱的。asyncio.gather
会保持原tasks
的顺序。
代码示例:
import asyncio
import random
async def foo(i):
# 等他思考一下
sleep_time = random.randint(0, 5)
await asyncio.sleep(sleep_time)
# 思考期间如果有别的算出来了可以先算喊出来
ans = i ** 2
print(ans)
return ans
async def main():
data = list(range(10))
# 等所有的都算出来保存到results
results = await asyncio.gather(*list(map(foo, data)))
for d, result in zip(data, results):
print(f'{d}: {result}')
if __name__ == '__main__':
asyncio.run(main())
1.4.协程版本改装
原本写好的单线程版本已经写好了能不能低成本的改造成协程?我目前的理解是可以的,但是不知道这样改造会不会有暗坑?或许还可以改个装饰器的版本使用起来会更优雅,还没尝试过,后续可能会补充。
import asyncio
import time
from functools import partial
def foo(*args, **kwargs):
time.sleep(3)
print('hello')
return True
async def async_foo(*args, **kwargs):
try:
loop = asyncio.get_event_loop()
result = await loop.run_in_executor(None, partial(foo, *args, **kwargs))
except Exception as exc:
logger.exception('')
result = False
return result
if __name__ == '__main__':
boolean = asyncio.run(async_foo())
print(boolean) # >> True
再给一个比较实操的例子:我有一个Cloud
类,下面有一个方法download_image
,我现在想基于download_image
改造一个协程版本的download_images
。
import asyncio
import tqdm
from loguru import logger
class Cloud:
def download_image(self, bucket_name: str = None, image_key: str = None, save_dir: str = 'images') -> None:
"""
input:
bucket_name: 图片所在桶
image_key: 图片在桶内相对路径
save_dir: 本地文件下载位置
return:
None
"""
pass
def download_images(self, image_keys, **save_params):
"""
input:
image_keys: 图片在桶内相对路径(列表、元组等可迭代对象)
bucket_name: 图片所在桶
save_dir: 本地文件下载位置
return:
None
"""
async def async_download_image(image_key, **kwargs):
try:
loop = asyncio.get_event_loop()
await loop.run_in_executor(None, partial(self.download_image, image_key=image_key, **kwargs))
except Exception as exc:
logger.error(f'{image_key} generated an exception: {exc}')
pbar.update()
async def async_download_images():
tasks = [async_download_image(image_key, **save_params) for image_key in filtered_image_keys]
await asyncio.gather(*tasks)
filtered_image_keys = list(filter(lambda x: isinstance(x, str), image_keys))
pbar = tqdm(total=len(filtered_image_keys), unit='images', unit_scale=True)
asyncio.run(async_download_images())