Python-异步IO

Python-异步IO

一 异步io的作用

  • tornado、fastapi、django3.x、aiohttp都在使用异步
  • 使用异步的原因是为了提升性能

二 来源

  • asyncio和异步编程都是基于协程来演变的

三 预备知识

1 协程

协程不是计算机提供的,是程序员认为创造出来的。也成为微线程,是一种用户态内的上下文切换技术。就是用一个线程实现代码块的相互切换运行。

实现方法:

  • greenlet,早期模块

  • yield关键字

    def func1():
        yield 1
        yield from func2()  # 这里要取值不能直接yield func2(),会返回对象而不是执行后的值
        yield 2
    
    
    def func2():
        yield 3
        yield 4
    
    
    f1 = func1()
    
    for item in f1:
        print(item)
        
    # 1
    # 3
    # 4
    # 2
    
    def func1():
        yield 1
        # yield from func2()  # 这里要取值不能直接yield func2(),会返回对象而不是执行后的值
        yield next(f2)
        yield 2
    
    
    def func2():
        yield 3
        yield next(f1)
        yield 4
    
    
    f1 = func1()
    f2 = func2()
    
    for item in f2:
        print(item)
    
  • gevent模块

  • asyncio装饰器(py3.4)

    import asyncio
    
    
    @asyncio.coroutine
    def func1():
        print(1)
        yield from asyncio.sleep(2)  # 遇到io耗时操作,自动化切换tasks中的其他任务
        print(2)
    
    
    @asyncio.coroutine
    def func2():
        print(3)
        yield  from asyncio.sleep(2)  # 只能用asuncio中的sleep
        print(4)
    
    
    tasks = [
        asyncio.ensure_future(func1()),
        asyncio.ensure_future(func2())
    ]
    
    loop = asyncio.get_event_loop()
    loop.run_until_complete(asyncio.wait(tasks))  # python会同时随机选择一个函数去执行
    # 牛逼之处在于他遇到io阻塞会自动切换,如果放到网络io请求,那么他的影响就很大
    
  • async/await关键字(3.5)【推荐】

    # 实际上功能逻辑与asyncio的功能逻辑是一样的,只是为了更加方便使用,python引入了asunc和await两个关键字
    import asyncio
    
    
    async def func1():
        print(1)
        await asyncio.sleep(2)  # 遇到io耗时操作,自动化切换tasks中的其他任务
        print(2)
    
    
    async def func2():
        print(3)
        await asyncio.sleep(2)
        print(4)
    
    
    tasks = [
        asyncio.ensure_future(func1()),
        asyncio.ensure_future(func2())
    ]
    
    loop = asyncio.get_event_loop()
    loop.run_until_complete(asyncio.wait(tasks))
    

2 协程的意义

在协程中,如果遇到io等待时间,线程不会傻傻等,利用等待时间去干其他事情

以协程的方式运行的异步编程

import requests
import asyncio
import aiohttp


# def download_image(url):
#     print("开始下载:", url)
#     # 发送网络请求
#     response = requests.get(url)  # 这里返回的不是协程函数、task或者future对象中的一种,所以不能使用await,即requests不支持协程异步
#     print("下载完成")
#
#     # 图片保存到本地文件
#
#     file_name = url.rsplit("_")[-1]
#     with open(file_name, "wb") as f:
#         f.write((response.content))


async def fetch(session, url):
    print("发送请求:", url)
    async with session.get(url, verify_ssl=False) as response:
        content = await response.content.read()
        file_name = url.rsplit("_")[-1]
        with open(file_name, "wb") as f:
            f.write(content)
        print("下载完成", url)


async def main():
    async with aiohttp.ClientSession() as session:
        url_list = [
            "https://www3.autoimg.cn/newsdfs/g26/M02/35/A9/120x90_0_autohomecar__ChsEe12AXQ6AOOH_AAFocMs8nzU621.jpg",
            "https://www3.autoimg.cn/newsdfs/g30/M01/3C/E2/120x90_0_autohomecar__ChcCSV2BBICAUntfAADjJFd6800429.jpg",
            "https://www3.autoimg.cn/newsdfs/g26/M0B/3C/65/120x90_0_autohomecar__ChcCP12BFCmAIO83AAGq7vK0sGY193.jpg"
        ]
        # tasks = [asyncio.create_task(fetch(session, url)) for url in url_list]  # 报错:module 'asyncio' has no attribute 'create_task'
        tasks = [asyncio.get_event_loop().create_task(fetch(session, url)) for url in url_list]


        await asyncio.wait(tasks)


if __name__ == '__main__':
    # 报错原因:Python 版本低于 3.7
    # asyncio.run(main())  # 报错:AttributeError: module 'asyncio' has no attribute 'run'
    asyncio.get_event_loop().run_until_complete(main())
    # python 3.7 以前的版本调用异步函数的步骤:
    # 1、调用asyncio.get_event_loop()函数获取事件循环loop对象
    # 2、通过不同的策略调用loop.run_forever()方法或者loop.run_until_complete()方法执行异步函数

四 异步编程

1 事件循环

事件循环:可以理解为一个死循环,去检测并执行某些代码

# 伪代码
任务列表 = [ 任务1, 任务2, 任务3 ...]

while: True:
        可执行的任务列表, 已完成的任务列表 = 去任务列表中检查所有的任务,将“可执行”和“已完成”的任务返回
        
        for 就绪任务 in 可执行的任务列表:
        	执行已就绪的任务
            
        for 已完成的任务 in 已完成的任务列表:
        	在任务列表中移除已完成的任务
            
        如果 任务列表 中的任务都已经完成,则终止循环
import asyncio

# 去生成或获取一个事件循环
loop = asyncio.get_event_loop()

# 将任务放到“任务列表”
loop.run_until_complete(任务)

uvloop:uvloop是asyncio的事件循环的替代方案。

pip3 install uvloop  # 需要python7版本以上
import asyncio
import uvloop
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())

# 在这里编写asyncio的代码,与之前写的代码一直。

# 内部的时间循环自动化会变为uvloop
asyncio.run(...)

注意:一个asgi---->uvicorn内部使用的就是uvloop

2 快速上手

协程函数:定义函数时候async def 函数名

协程对象:执行协程函数() 得到的协程对象。

async def func():
    pass

result = func()  # 注意:执行协程函数创建协程对象,函数内部代码不会执行
import asyncio

async def func():
    print("协程函数内部")

result = func()

loop = asyncio.get_event_loop()
loop.run_until_complete(result)  # 注意:如果要运行协程函数内部代码,必须将协程对象交给事件循环来处理

# python3.7之后可以使用
# asyncio.run(result)

3 await

await + 可等待的对象(协程对象、Future对象、Task对象)-->IO等待

示例1:

import asyncio


async def func():
    print("开始")
    response = await asyncio.sleep(2)  # 它里面本质是做了一个future对象的封装
    print("结束", response)


asyncio.get_event_loop().run_until_complete(func())

示例2:

import asyncio

async def others():
    print("开始")
    response = await asyncio.sleep(2)
    print("结束", response)
    return "返回值"

async def func():
    print("执行协程函数内部代码")

    # 遇到IO操作挂起当前协程(任务),等IO操作完成之后再继续往下执行。当前协程挂起时,事件循环可以去执行其他协程
    response = await others()  # 可以这样获得返回值
    print("IO请求结束,结果为:", response)

asyncio.get_event_loop().run_until_complete(func())

示例3:

import asyncio


async def others():
    print("开始")
    response = await asyncio.sleep(2)
    print("结束", response)
    return "返回值"


async def func():
    print("执行协程函数内部代码")

    # 遇到IO操作挂起当前协程(任务),等IO操作完成之后再继续往下执行。当前协程挂起时,事件循环可以去执行其他协程
    response1 = await others()
    print("IO请求结束,结果为:", response1)

    response2 = await others()
    print("IO请求结束,结果为:", response2)


asyncio.get_event_loop().run_until_complete(func())

await就是等待对象的值,等到结果之后再继续向下走。

4 Task对象

Tasks are used to schedule coroutines concurrently.
When a coroutine is wrapped into a Task with functions like asyncio.create_task() the coroutine is automatically scheduled to run soon.

白话:在事件循环中添加多个任务的。

Tasks 用于并发调度协程,通过asyncio.create_task(协程对象)的方式创建 Task 对象,这样可以让协程加入事件循环中等待被调度执行。除了使用 asyncio.create_task()函数以外,还可以用低层级的loop.create_task()ensure_future()函数。不建议手动实例化Task对象。

注意:asyncio.create_task()函数在 Python 3.7 中被加入.在 Python 3.7 之前,可以改用低层级的ensure_future()函数。

示例1:

import asyncio


async def func(value):
    print(value)
    await asyncio.sleep(2)
    print(value)
    return "返回值"


async def main():
    print("main开始")

    # 创建Task对象,将当前执行func函数任务添加到事件循环
    task1 = asyncio.get_event_loop().create_task(func("task1"))
    task2 = asyncio.ensure_future(func("task2"))
    # task2 = asyncio.create_task(func())  # 版本python3.7或以上

    print("main结束")

    # 当执行某协程遇到IO操作时,会自动化切换执行其他任务
    # 此处的await是等待相对应的协程全部执行完毕并获取结果
    ret1 = await task1
    ret2 = await task2
    print(ret1, ret2)


asyncio.get_event_loop().run_until_complete(main())

示例2:

import asyncio


async def func(value):
    print(value)
    await asyncio.sleep(2)
    print(value)
    return "返回值"


async def main():
    print("main开始")

    # 创建Task对象,将当前执行func函数任务添加到事件循环
    # task1 = asyncio.get_event_loop().create_task(func("task1"))
    # task2 = asyncio.ensure_future(func("task2"))
    # task2 = asyncio.create_task(func())  # 版本python3.7或以上
    tasks_list = [
        asyncio.get_event_loop().create_task(func("task1")),
        asyncio.get_event_loop().create_task(func("task2"))
    ]

    print("main结束")

    # 当执行某协程遇到IO操作时,会自动化切换执行其他任务
    # 此处的await是等待相对应的协程全部执行完毕并获取结果
    # ret1 = await task1
    # ret2 = await task2
    done, pending = await asyncio.wait(tasks_list, timeout=None)
    for _ in range(len(done)):
        print(done.pop().result())  # 获取返回值
    print(pending)


asyncio.get_event_loop().run_until_complete(main())

示例3:用的比较多

import asyncio


async def func(value):
    print(value)
    await asyncio.sleep(2)
    print(value)
    return "返回值"


def main():
    print("main开始")

    # 创建Task对象,将当前执行func函数任务添加到事件循环
    # task1 = asyncio.get_event_loop().create_task(func("task1"))
    # task2 = asyncio.ensure_future(func("task2"))
    # task2 = asyncio.create_task(func())  # 版本python3.7或以上
    tasks_list = [
        func("task1"),
        func("task2")
    ]

    done, pending = asyncio.get_event_loop().run_until_complete(asyncio.wait(tasks_list))

    print("main结束")

    # 当执行某协程遇到IO操作时,会自动化切换执行其他任务
    # 此处的await是等待相对应的协程全部执行完毕并获取结果
    # ret1 = await task1
    # ret2 = await task2
    # done, pending = await asyncio.wait(tasks_list, timeout=None)
    for _ in range(len(done)):
        print(done.pop().result())  # 获取返回值
    print(pending)


if __name__ == '__main__':
    main()

5 asyncio.Future对象

A Future is a special low-level awaitable object that represents an eventual result of an asnchronous operation

Task继承Future,Task对象内部await结果的处理基于Future对象来的

示例1:

import asyncio


async def main():
    # 获取当前事件循环
    loop = asyncio.get_event_loop()

    # 创建一个任务(Future对象),这个任务什么都不干
    fut = loop.create_future()

    # 等待任务最终结果(Future对象),没有结果则会一直等下去
    await fut


asyncio.get_event_loop().run_until_complete(main())
# asyncio.run(main())

示例2:

import asyncio


async def set_after(fut):
    await asyncio.sleep(2)
    fut.set_result("666")


async def main():
    # 获取当前事件循环
    loop = asyncio.get_event_loop()

    # 创建一个任务(Future对象),没绑定任何行为,则这个任务永远不知道什么时候会结束
    fut = loop.create_future()

    # 创建一个任务(Task独享),绑定了set_after函数,函数内部在2s之后,会给fut赋值。
    # 即手动设置future任务的最终结果,那么fut就可以结束了。
    await loop.create_task(set_after(fut))

    # 等待任务最终结果(Future对象),没有结果则会一直等下去
    result = await fut
    print(result)


asyncio.get_event_loop().run_until_complete(main())

6 concurrent.futures.Future对象

使用线程池、进程池实现异步操作时用到的对象

import time
from concurrent.futures import Future
from concurrent.futures.thread import ThreadPoolExecutor
from concurrent.futures.process import ProcessPoolExecutor


def func(value):
    time.sleep(1)
    print(value)


# 创建线程池
pool = ThreadPoolExecutor(max_workers=5)

# 创建进程池
# pool = ProcessPoolExecutor(max_workers=5)

for i in range(10):
    fut = pool.submit(func, i)
    print(fut)

以后写代码可能会存在交叉使用的时候,例如:crm项目80%都是基于协程异步编程+MySQL(不支持)【线程、进程做异步编程)

import time
import asyncio
import concurrent.futures


def func1():
    # 某个耗时操作
    time.sleep(2)
    return "返回值"


async def main():
    loop = asyncio.get_event_loop()

    # 1.Run in the default loop's executor(默认ThreadPoolExecutor)
    # 第一步:内部会先调用ThreadPoolExecutor的submit方法去线程池中申请一个线程去执行func1函数,并返回一个concurrent.futures.Future对象
    # 第二部:调用asyncio.wrap_future将concurrent.futures.Future对象包装为asycio.Future对象。
    # 因为concurrent.futures.Futures对象不支持await语法,所以需要包装为asycio.Future对象才能使用
    fut = loop.run_in_executor(None, func1)
    result = await fut
    print("default thread pool", result)

    # 2.Run in a custom thread pool:
    with concurrent.futures.ThreadPoolExecutor() as thread_pool:
        result = await loop.run_in_executor(thread_pool, func1)
        print("custom threadpool", result)

    # 3.Run in a custom process pool:
    with concurrent.futures.ProcessPoolExecutor() as process_pool:
        result = await loop.run_in_executor(process_pool, func1)
        print("custom process pool", result)

if __name__ == '__main__':

    asyncio.get_event_loop().run_until_complete(main())
    # concurrent.futures.process.BrokenProcessPool: A process in the process pool was terminated abruptly while the future was running or pending.
    # 多进程concurrent.futures的ProcessPoolExecutor需要在"if name == main"下运行,否则会报错
    # 因为当一个子进程诞生后,会重读代码,读到“with ProcessPoolExecutor() as Exe:”时,在此创建了子进程,而多进程模块不允许子进程中再次创建子进程,所以报错

案例:asyncio+不支持异步的模块

import requests
import asyncio


async def download_image(url):
    print("开始下载:", url)

    loop = asyncio.get_event_loop()
    # requests模块默认不支持异步操作,所以就使用线程池来配合实现了
    future = loop.run_in_executor(None, requests.get, url)

    # 发送网络请求
    response = await future
    print("下载完成")

    # 图片保存到本地文件
    file_name = url.rsplit("_")[-1]
    with open(file_name, "wb") as f:
        f.write((response.content))


if __name__ == '__main__':
    url_list = [
        "https://www3.autoimg.cn/newsdfs/g26/M02/35/A9/120x90_0_autohomecar__ChsEe12AXQ6AOOH_AAFocMs8nzU621.jpg",
        "https://www3.autoimg.cn/newsdfs/g30/M01/3C/E2/120x90_0_autohomecar__ChcCSV2BBICAUntfAADjJFd6800429.jpg",
        "https://www3.autoimg.cn/newsdfs/g26/M0B/3C/65/120x90_0_autohomecar__ChcCP12BFCmAIO83AAGq7vK0sGY193.jpg"
    ]
    tasks = [download_image(url) for url in url_list]
    loop = asyncio.get_event_loop()
    loop.run_until_complete(asyncio.wait(tasks))
    # 报错原因:Python 版本低于 3.7
    # asyncio.run(main())  # 报错:AttributeError: module 'asyncio' has no attribute 'run'
    # asyncio.get_event_loop().run_until_complete(main())

7 异步迭代器

  • 什么是异步迭代器

    实现了__aiter__()和__anext__()方法的对象。__anext__必须返回一个awaitable对象。async_for会处理异步迭代器的__anext__()方法所返回的可等待对象、指导其引发一个StopAsyncIteration异常。由PEP 429引入。

  • 什么是异步可迭代对象?

    可在async_for语句中被使用的对象。必须通过它的__aiter__()方法返回一个asynchronous iterator。由PEP 492引入

示例1:

import asyncio


class Reader(object):
    """
    自定义异步迭代器(同时也是异步可迭代对象)
    """
    def __init__(self):
        self.count = 0

    # async def readline(self):


    def __aiter__(self):
        return self

    async def __anext__(self):
        await asyncio.sleep(1)
        self.count += 1

        if self.count == 10:
            raise StopAsyncIteration

        return self.count

# 必须把async for写入一个协程函数中,不然会报错
async def func():
    obj = Reader()
    async for item in obj:
        print(item)


tasks_list = [func() for _ in range(4)]  # 这样就能实现多个io等待的for循环异步迭代


loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks_list))

8 异步上下文管理器

此种对象通过定义__aenter__()和__aexit__()方法来对async with语句中的环境进行控制。由PEP 492引入

一般需要打开、关闭的操作都会有模块去实现类似的功能,但是当出现打开、关闭操作的时候,脑海里要有异步实现的概念

示例1:

import asyncio


class AsyncContextManager:
    def __init__(self, count):
        self.count = count

    async def do_something(self):
        # 异步操作
        return 666

    async def __aenter__(self):
        # 异步链接数据库
        await asyncio.sleep(1)
        print(self.count, "打开")
        return self

    async def __aexit__(self, exc_type, exc_val, exc_tb):
        # 关闭数据库链接
        await asyncio.sleep(1)
        print(self.count, "关闭")


# 必须把with写入一个协程函数中,不然会报错
async def func(count):
    async with AsyncContextManager(count) as f:
        result = await f.do_something()
        print(result)

tasks_list = [func(count) for count in range(4)]


loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks_list))

五 案例

1 异步操作redis

在使用python代码操作redis时,链接/操作/断开都是网络IO

pip3 install aioredis

示例1:

import asyncio
import aioredis


async def execute(address):
    print("开始执行", address)

    redis = await aioredis.create_redis(address, password="redis")
    # 网路IO操作:在redis中设置哈值car,内部再设三个键值对,即redis = {car:{ keyf1:1, key2:2, key3:3}}
    await redis.hmset_dict("car", key1=1, key2=2, key3=3)

    # 网络IO操作,去redis中获取值
    result = await redis.hgetall("car", encoding='utf-8')
    print(result)

    redis.close()

    # 网络操作,关闭redis连接
    await redis.wait_closed()

    print("结束", address)


loop = asyncio.get_event_loop()
loop.run_until_complete(execute("redis://192.168.20.133:6379"))

示例2:

import asyncio
import aioredis


async def execute(address):
    print("开始执行", address)

    redis = await aioredis.create_redis(address, password="redis")
    # 网路IO操作:在redis中设置哈值car,内部再设三个键值对,即redis = {car:{ keyf1:1, key2:2, key3:3}}
    await redis.hmset_dict("car", key1=1, key2=2, key3=3)

    # 网络IO操作,去redis中获取值
    result = await redis.hgetall("car", encoding='utf-8')
    print(result)

    redis.close()

    # 网络操作,关闭redis连接
    await redis.wait_closed()

    print("结束", address)

task_list = [
    execute("redis://192.168.20.133:6379"),
    execute("redis://192.168.20.133:6379")  # 这里改成第二台redis
]

loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(task_list))

2 异步MySQL

pip3 install aiomysql
import asyncio
import aiomysql


async def execute():
    # 网络IO操作:连接数据库
    conn = await aiomysql.connect(host="127.0.0.1", port=3306, user="root", password="mysql")

    # 网络IO操作:chuangjian CURSOR
    cur = await conn.cursor()

    # 网络IO操作:执行SQL
    await cur.execute("use mysql")
    await cur.execute("select Host, User from user")

    # 网络IO操作:获取SQL结果
    result = await cur.fetchall()
    print(result)

    # 网络IO,关闭连接
    await cur.close()
    conn.close()


loop = asyncio.get_event_loop()
loop.run_until_complete(execute())

示例2:

import asyncio
import aiomysql


async def execute(host, password):
    # 网络IO操作:连接数据库
    conn = await aiomysql.connect(host=host, port=3306, user="root", password=password)

    # 网络IO操作:chuangjian CURSOR
    cur = await conn.cursor()

    # 网络IO操作:执行SQL
    await cur.execute("use mysql")
    await cur.execute("select Host, User from user")

    # 网络IO操作:获取SQL结果
    result = await cur.fetchall()
    print(result)

    # 网络IO,关闭连接
    await cur.close()
    conn.close()

tasks_list = [
    execute("127.0.0.1", "mysql"),
    execute("127.0.0.1", "mysql")  # 换成别的mysql
]

loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks_list))

3 FastAPI框架

pip3 install fastapi
pip3 install uvicorn(asgi内部基于uvloop)

示例1:

import asyncio
import uvicorn
import aioredis
from fastapi import FastAPI
from aioredis import Redis
import time

app = FastAPI()

# 创建一个redis连接池
REDIS_POOL = aioredis.ConnectionsPool("redis://192.168.20.133:6379", password="redis", minsize=1, maxsize=10)

@app.get("/")
def index():
    """
    普通操作接口
    :return:
    """
    time.sleep(10)
    return {"message": "Hello world"}

@app.get("/red")
async def red():
    """
    异步操作接口
    :return:
    """
    print("red请求来了")

    await asyncio.sleep(10)
    conn = await REDIS_POOL.acquire()
    redis = Redis(conn)

    # 设置值
    await redis.set("chen:2", "salve")

    # 读取值
    result = await redis.get("chen:2")
    print(result)

    # 连接归还链接池
    REDIS_POOL.release(conn)

    return result

if __name__ == '__main__':
    uvicorn.run("main:app", host="192.168.3.40", port=5000, log_level="info")  # main是启动文件名

4 爬虫

pip3 install aiohttp
import aiohttp
import asyncio

async def fetche(session, url):
    print("发送请求:", url)
    async with session.get(url, verify_ssl=False) as response:
        text = await response.text()
        print("得到结果:", url, len(text))

async def main():
    async with aiohttp.ClientSession() as session:
        url_list = [
            "https://python.org",
            "https://www.baidu.com",
            "https://www.pythonav.com"
        ]

        tasks = [asyncio.get_event_loop().create_task(fetche(session, url)) for url in url_list]

        done, pending = await asyncio.wait(tasks)
        print(done)


if __name__ == '__main__':
    asyncio.get_event_loop().run_until_complete(main())

六 总结

  • 使用异步的原因是为了提升性能,异步IO最大的意义:通过一个线程利用其IO等待时间去做一些其他的事情asyncio和异步编程都是基于协程来演变的
  • python中使用async/await关键字可以实现异步编程;功能逻辑与asyncio的功能逻辑是一样的,只是为了更加方便使用,python引入了asunc和await两个关键字
  • 异步编程:事件循环可以理解为一个死循环,去检测并执行某些代码;await 后面接可等待的对象(协程对象、Future对象、Task对象)-->IO等待;Tasks 在事件循环中添加多个任务的,用于并发调度协程;Task继承Future,Task对象内部await结果的处理基于Future对象来的,用于接收协程对象的结果
  • 在模块不支持协程异步的时候可以使用线程/进程做异步
posted @   陈俊明  阅读(246)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
点击右上角即可分享
微信分享提示