python 异步 asyncio_4: 应用1

前文中,我们已经大致了解了python异步的原理。其实python异步只需要抓住两点:1.事件循环是核心,2.任务调度需要在事件循环的任务队列中,就可以了。其他类似实现细节,如事件回调机制,可等待(awaitable)对象,async/await语法糖等等,在这两点的基础上加以理解即可。
接下来,就要来了解一下,异步在实际开发中有哪些应用。其实在实际开发中,我们接触最多的,一个是网络请求,一个是数据库请求,除此之外还有一些消息队列缓存库等等,大多都是网络IO。
而python的好处就是,它拥有完整的生态圈,很多库都是现成的。比如网络请求异步库:aiohttp, mysql数据库aiomysql, redis的异步库aioredis等等。
当然,下面的内容并不是单纯地介绍这些库的用法,更多的是一些异步思路,和异步实际开发中的一些细节。
首先,从网络请求开始。

异步web请求,aiohttp

基本用法:

import asyncio
import aiohttp
from aiohttp import ClientSession

async def fetch_status(session: ClientSession, url: str)-> int:
    ten_millis = aiohttp.ClientTimeout(total=.1)
    async with session.get(url, timeout=ten_millis) as result:
        return result.status

async def main():
    session_timeout = aiohttp.ClientTimeout(total=1, connect=.1)
    async with aiohttp.ClientSession(timeout=session_timeout)as session:
        await fetch_status(session, 'https://example.com')

asyncio.run(main())

● total=1:这个参数设置了整个请求(包括连接、读取响应等所有步骤)的总超时时间为1秒。如果整个请求在1秒内没有完成,那么将会抛出一个超时异常。
● connect=.1:这个参数设置了连接到服务器的超时时间为0.1秒(即100毫秒)。如果在这个时间内无法建立与服务器的连接,那么也会抛出一个超时异常。

上述代码用到的aiohttp的ClientSession,通过session来异步请求。与requests中的session类似。
但上述代码只是简单地进行演示,我们真正进行多请求并发执行时,还是需要将请求的方法封装成task。
在之前,我们使用过create_task这个方法,并且提到,遇到await就会开始调度,因此,如果要同时进行1000次请求,示例如下:

import asyncio
import aiohttp
from aiohttp import ClientSession

async def fetch_status(session: ClientSession, url: str)-> int:
    ten_millis = aiohttp.ClientTimeout(total=.1)
    async with session.get(url, timeout=ten_millis) as result:
        return result.status
async def main():
    async with aiohttp.ClientSession()as session:
        urls = ['https://example.com' for _ in range(1000)]
        tasks = [asyncio.create_task(fetch_status(session, url)) for url in urls]
        [await task for task in tasks]
asyncio.run(main())

异步请求和相关方法

上述代码可以实现同时进行1000个请求访问的目的,但是存在问题,一个是比较麻烦,需要创建task,第二是很难进行异常处理,不知道那个协程出现了异常,而且协程异常会导致其他任务终止。

gather

这里可以用到我们前面提到的asyncio的gather方法:

import asyncio
import aiohttp
from aiohttp import ClientSession

async def fetch_status(session: ClientSession, url: str)-> int:
    ten_millis = aiohttp.ClientTimeout(total=.1)
    async with session.get(url, timeout=ten_millis) as result:
        return result.status
async def main():
    async with aiohttp.ClientSession()as session:
        urls = ['https://example.com' for _ in range(1000)]
        requests = [fetch_status(session, url) for url in urls]
        status_codes = asyncio.gather(*requests)
asyncio.run(main())

gather 会自动将协程对象包装成task,它会等待所有的写成对象执行完成后,返回结果的列表, 使用时可以使用return_exceptions参数: 如果为True,协程发生的异常会作为返回值在列表中,不会抛出异常,如果为False,则gather会抛出异常,需要手动处理,否则将会中断程序

但是gather也有一些不足,由于结果是一个列表,因此,需要等到协程全部执行完成之后,才能访问结果。某些用时较短的协程结果也要等其他协程执行完成之后才能拿到。

as_complete

import asyncio
import aiohttp
from aiohttp import ClientSession

async def fetch_status(session: ClientSession, url: str)-> int:
    ten_millis = aiohttp.ClientTimeout(total=.1)
    async with session.get(url, timeout=ten_millis) as result:
        return result.status
async def main():
    async with aiohttp.ClientSession()as session:
        tasks = [fetch_status(session, "https://www.example.com") for _ in range(3)]
        for t in asyncio.as_complete(tasks):
            print(await t)
asyncio.run(main())

对此,asyncio还提供了一个as_complete方法。

as_complete方法,接收一个协程列表,返回的一个future迭代器,可以等待其中每一个协程的返回结果,协程仍然是并发执行,但是先结束的协程会先返回,通过循环await迭代器可以获取协程结果

但这个方法也有不足,首先,先完成先返回,也就是结果是无序的,我们无法知道返回的结果对应的是哪个协程。第二,这个方法也可以设置超时,超时会正确抛出异常,但是所有任务仍然在后台运行,如果想取消,很难确定哪些是超时任务,哪些正常执行。

wait

因此,asyncio提供了一个wait函数,这个方法返回两个集合:1.已完成任务的结果或异常,2.未完成的、仍在运行的任务,也可以指定超时,但是不会抛出异常

wait接收的是一个可等待对象列表,最好是task,timeout参数和return_when参数,return_when选择有(ALL_COMPLETED, FIRST_COMPLETED, FIRST_EXCEPTION,默认是ALL_COMPLETED)wait返回的done集合中,可以通过task.result()获取结果,或者通过task.exception()判断是否异常当我们使用FIRST_EXCEPTION,或者ALL_COMPLETED的时候,就可能出现等同于gather的情况,因此,如果想要实现类似使用as_complete的效果,可以使用FIRST_COMPLETED,然后循环pending集合,就可以了。

wait_for如果设置超时时间,则超时了就会取消任务,但是wait,gather,as_completed则不会,需要手动取消任务wait发生异常也不会自动抛出,与使用as_completed和gather的return_exceptions=True一样

async def sp1():
    await asyncio.sleep(1)
    # raise Exception("fuck")
    return 1

async def sp2():
    await asyncio.sleep(2)
    return 2

async def sp3():
    await asyncio.sleep(3)
    return 3

async def main():
    tasks = [asyncio.create_task(sp1()), asyncio.create_task(sp2()), asyncio.create_task(sp3())]
    # tasks = [sp1(),sp2(),sp3()]
    done, pending = await asyncio.wait(tasks, timeout=1.5)
    for j in pending:
        if j is tasks[1]:
            print("1 too long")
            j.cancel()

其他

与网络请求一样,异步请求数据库原理和用法也类似。只不过数据库中还有个连接池的概念,但大体用法是没有差别的。都需要我们把协程/任务丢到事件循环中去调度,利用的方法也是上面那几个。具体库的使用,就去看指定的库文档即可,这里不多赘述。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值