python协程(asyncio)学习笔记

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())
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值