Python 协程

一、前言

其实这块东西有点多,从iterator,iterable,yield,yield from,再到最终的async,await,asyncio
经历了python2.x -> 3.x的版本迭代,一篇文章也很难讲清楚,本文章主要说明在实际开发中怎么使用协程

支持协程所有指令的python版本:python >= 3.7
支持协程的python web框架:Django 3.x,Flask 2.x,fastAPI
支持协程的python 库:想实现什么功能建议直接去Google搜索

二、介绍

2.1 为什么要用协程

线程遇到IO阻塞是由系统调度的;协程是由用户调度的,更快更省资源
线程启动的时候需要定义线程数;协程则不需要,它能运行更多的任务

2.2 协程的效率受到哪些限制

协程是在线程内部运行,所以也受到GIL全局解释器锁的限制,无法同时利用多核CPU
协程需要有对应的第三方异步库的支持,否则得自己去使用协程的方法实现
协程是为了运行多任务的存在,如果使用协程只是执行单个任务,那它其实并不能提升效率
协程相比线程池,实现更复杂一点

2.3 协程的意义

通过一个线程利用其IO等待时间去做更多的事情

三、协程示例

这里为什么不用requests,因为python的requests库不支持协程,就像上面讲的要使用协程,也得需要web框架支持。

import asyncio
import aiohttp

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

async def main():
    async with aiohttp.ClientSession() as session:
        url_list = [
            'https://www.raycloud.com/r/cms/www/default/images/clientele_logo/logo_65.png',
            'https://www.raycloud.com/r/cms/www/default/images/clientele_logo/logo_66.png',
            'https://www.raycloud.com/r/cms/www/default/images/clientele_logo/logo_67.png'
        ]

        tasks = [asyncio.create_task(fetch(session, url)) for url in url_list]
        await asyncio.wait(tasks)

if __name__ == '__main__':
    asyncio.run(main())

四、事件循环

简单点说,事件循环可以理解为统一调度协程中一批任务的队列,将这协程任务丢到这个队列,进行调度;至于什么时候调度,取决于await语句;而async语句声明这是一个协程任务

import asyncio

tasks = list()
# 去生成或获取一个事件循环
loop = asyncio.get_event_loop()
# 将任务放到列表
loop.run_until_complete(tasks)

五、协程声明

协程函数: async def foo()
协程对象: 执行协程函数得到的对象,如: foo()
特别注意: 当协程函数func()后,内部代码是不会执行的,只是得到了一个协程对象(这是和普通函数的区别)
如果想要运行协程对象,则必须将其交给事件循环来处理

python3.7 + 的写法这里为什么没有定义事件循环? 因为asyncio.run(result) 在执行的时候,会默认创建一个事件循环;简化了之前版本的写法,所以协程使用是很简单的

async def func():                    # func为协程函数
    print('开始执行')

result = func()                      # result为协程对象

# python 3.7之前的写法
# loop = asyncio.get_event_loop()
# loop.run_until_complete(result)    # 让事件循环去执行协程对象

# python 3.7+写法
asyncio.run(result)

六、await

await 后面定义可等待的东西 (可以是:协程对象,Future,Task对象);这里的可等待东西就是IO等待
await 就是等待可等待的对象的值返回以后再放下走,在await的同时,从当前的协程任务切换到另一个就绪的协程任务
result = await 则表示result是await后面的指令返回结果,也就是一个return的对象

import asyncio
async def func():
    print('过来呀')
    response = await asyncio.sleep(2)
    print('已结束, ', response)

asyncio.run(func())
import asyncio

async def others():
    print('start')
    await asyncio.sleep(2)
    print('end')
    return '返回'

async def func():
    print('开始执行协程函数内部代码')
    # 当遇到IO操作挂起当前协程(任务),等IO操作完成之后再继续往下执行。当前协程挂起时,事件循环可以去执行其他协程(任务)
    response = await others()
    print('结束执行协程函数内部代码 ', response)

asyncio.run(func())

七、Task对象

前面的示例都是将一个协程函数添加到事件循环中,这其实没什么意义,因为使用协程本质上来讲是运行多个批量的任务。Task对象就是将一组协程对象;Tasks用于并发调度协程,通过asyncio.create_task(协程对象) 的方式创建Task对象,这样可以让协程加入到事件循环中等待被调度执行。

除了使用async.create()函数以外,还可以用低层级的 loop.create_task() 或ensure_future() 函数。不建议手动实例化Task对象

注意:asyncio.create_task() 函数在python3.7中被加入。在python3.7之前,可以改用低层级的 asyncio.ensure_future()函数,python官方建议用户使用高层级的方法

7.1 单任务

下面是使用task对象的简单方法,将一个一个任务添加到事件循环的。

import asyncio
async def func():
    print(1)
    await asyncio.sleep(2)
    print(2)
    return '返回值'

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

    # 创建协程对象,将协程对象封装到一个Task对象中并立即添加到事件循环的任务列表中,等待事件循环去执行(默认是就绪状态)
    task1 = asyncio.create_task(func())
    task2 = asyncio.create_task(func())

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

7.2 多任务

下面是列出了一组tasks任务,一次性添加到携程事件循环中

import asyncio
async def func():
    print(1)
    await asyncio.sleep(2)
    print(2)
    return '返回值'

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

    task_list = [
        asyncio.create_task(func(), name='t1'),
        asyncio.create_task(func(), name='t2')
    ]
    print('main结束')

    done, pending = await asyncio.wait(task_list, timeout=3)
    print(done, pending)

asyncio.run(main())

7.3 多任务优化

下面是上面方法的简写,使代码更简洁

import asyncio
async def func():
    print(1)
    await asyncio.sleep(2)
    print(2)
    return '返回值'

task_list = [func() for _ in range(5)]
done, pending = asyncio.run(asyncio.wait(task_list))
print('done: ', done)
print('pending: ', pending)

八、asyncio.future对象

Future是一个更低层的接口,用来等待异步的执行结果
Task继承Future对象,Task对象内部await结果的处理基于Future对象来的。

8.1 语法

import asyncio

async def main():
    # 获取当前运行的事件循环
    loop = asyncio.get_running_loop()

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

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

asyncio.run(main())

8.2 示例

import asyncio
import time

async def set_after(future):
    await asyncio.sleep(2)
    future.set_result('666')

async def main():
    start_time = time.time()

    # 获取当前事件循环
    loop = asyncio.get_running_loop()

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

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

    # 等待Future对象获取最终结果,否则一直等下去
    data = await future
    print(data)
    print(time.time() - start_time)

asyncio.run(main())

九、concurrent.futures.Future 对象

使用线程池、进程池实现异步操作时用到的对象。
线程池:from concurrent.futures import ThreadPoolExecutor
进程池:from concurrent.futures import ProcessPoolExecutor

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

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

if __name__ == '__main__':
    # 创建线程池
    with ThreadPoolExecutor() as pool:
        for i in range(12):
            fut = pool.submit(func, i)
            print(fut)

    # 创建进程池
    with ProcessPoolExecutor() as pool:
        for i in range(12):
            fut = pool.submit(func, i)
            print(fut)

十、实现线程协程和进程协程

以后写代码可能会存在交叉使用
例如:crm项目80%都是基于协程异步编程+MySQL(不支持)【线程、进程做异步编程】
当有的第三方库不支持协程,那么就需要两种结合

import time
import asyncio
import concurrent.futures

def func1():
    # 某个耗时操作
    time.sleep(2)
    return 'ok'

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

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

    # 运行一个线程池
    with concurrent.futures.ThreadPoolExecutor() as pool:
        result = await loop.run_in_executor(pool, func1)
        print('custom thread pool: ', result)

    # 运行一个进程池
    with concurrent.futures.ProcessPoolExecutor() as pool:
        result = await loop.run_in_executor(pool, func1)
        print('custom process pool: ', result)

if __name__ == '__main__':
    asyncio.run(main())

十一、爬虫示例:asyncio + 不支持异步的模块

由于requests是不支持协程的,所以为了使用协程提升效率,则可以使用loop.run_in_executor()方法
下面的场景其实在多线程和协程分别执行的时候,效率其实提升不了多少,因为文件很小本身就很快,没有遇到多少阻塞;那在什么情况下会有很大的区别呢,就是文件很大需要时间或者你会在进行sleep等待浏览器程序加载前端页面的时候

11.1 loop.run_in_executor

下面使用loop.run_in_executor()执行

import asyncio
import requests

async def download_image(url):
    # 发送网络请求,下载图片(遇到网络下载图片的IO请求,自动化切换到其他任务)
    print('开始下载: ', url)

    loop = asyncio.get_event_loop()
    future = loop.run_in_executor(None, requests.get, url)
    response = await future
    
    print('下载完成')

    # 图片保存到本地
    file_name = f"/Users/yuehua/Downloads/images/{url.split('_')[-1]}"
    with open(file_name, 'wb') as f_obj:
        f_obj.write(response.content)

if __name__ == '__main__':
    url_list = [f'https://www.raycloud.com/r/cms/www/default/images/clientele_logo/logo_{i}.png' for i in range(60)]
    tasks = [download_image(url) for url in url_list]
    loop = asyncio.get_event_loop()
    loop.run_until_complete(asyncio.wait(tasks))

11.2 协程

import asyncio
import requests
import time

async def download_image(url):
    start_time = time.time()
    # 发送网络请求,下载图片(遇到网络下载图片的IO请求,自动化切换到其他任务)
    loop = asyncio.get_event_loop()
    response = await loop.run_in_executor(None, requests.get, url)

    # 模拟等待浏览器前端页面加载
    await asyncio.sleep(5)
    
    # 图片保存到本地
    file_name = f"/Users/yuehua/Downloads/images/{url.split('/')[-1]}"
    with open(file_name, 'wb') as f_obj:
        f_obj.write(response.content)
    print(time.time() - start_time)

# 用该方式获取result,不过python3.11中将删除该方式
# async def main():
#     url_list = [f'https://www.raycloud.com/r/cms/www/default/images/clientele_logo/logo_{i}.png' for i in range(60)]
#     tasks = [download_image(url) for url in url_list]
#     done, pending = await asyncio.wait(tasks)
#     print(done)

if __name__ == '__main__':
    url_list = [f'https://www.raycloud.com/r/cms/www/default/images/clientele_logo/logo_{i}.png' for i in range(60)]
    tasks = [download_image(url) for url in url_list]
    asyncio.run(asyncio.wait(tasks))
    # asyncio.run(main())

11.3 多线程

from concurrent.futures import ThreadPoolExecutor
import requests
import time

def url_save(url):
    start_time = time.time()

    response = requests.get(url)
    file_name = f"/Users/yuehua/Downloads/images/{url.split('/')[-1]}"

    # 模拟等待浏览器前端页面加载
    time.sleep(5)

    with open(file_name, 'wb') as f_obj:
        f_obj.write(response.content)

    return (time.time() - start_time)

if __name__ == '__main__':
    start_time = time.time()
    with ThreadPoolExecutor() as pool:
        url_list = [f'https://www.raycloud.com/r/cms/www/default/images/clientele_logo/logo_{i}.png' for i in range(60)]
        results = pool.map(url_save, tuple(url_list))
        for i in results:
            print('运行时间: ', i)

    print('总运行时间: ', time.time() - start_time)

如果你手动执行过上面线程和协程的代码,你会发现,当任务执行过程中需要等待IO的时候,协程与线程的效率有多不同

11.4 结论

只要任务队列不多,基本上忽略线程和协程上下文切换的时间(协程切换<线程切换),例如我们爬虫的总任务数=sum(需爬虫的账户*每个账户的资源标签)

  1. 单任务运行:时间=单任务时间*任务数
  2. 多线程运行:时间=单任务时间*任务数/线程数 + 线程切换时间
  3. 协程运行: 时间=单任务(异步执行时间) + 单任务(同步执行时间)*任务数量 + 协程切换时间
  4. 协程+多线程运行:时间=单任务(异步执行时间) + 单任务(同步执行时间)*任务数量/线程数量 + 线程切换时间 + 协程切换时间

顺便提一下python中的并行与并发,有时间单独拿出来介绍
多进程:并行运行。当CPU核心数>1,任务是会并行运行的,且可并行运行的任务数量等于核心数
多线程:并发运行。因为全局解释器锁GIL,只有当前正在运行的线程才持有锁,其他线程都被挂起
协程:并发运行。协程是在线程内部运行的,且上下文的切换是由用户控制,且同一时刻只能有一个任务在运行

十三、异步迭代器

什么是异步迭代器
实现了 aiter()和 anext()方法的对象。anext 必须返回一个 awaitable对象。async_for会处理异步迭代器的
anext()方法所返回的可迭代对象,直到其引发一个 StopAsyncIteration 异常。

什么是异步可迭代对象
可在 async_for 语句中被使用的对象。必须通过它的 aiter() 方法返回一个 asynchronousiterator

import asyncio

class Reader:
    """ 自定义异步迭代器(同时也是异步可迭代对象)"""

    def __init__(self):
        self.count = 0

    async def readline(self):
        # await asyncio.sleep(1)
        self.count += 1
        if self.cont == 100:
            return None
        return self.count
    
    def __aiter__(self):
        return self
    
    async def __anext__(self):
        val = await self.readline()
        if val == None:
            raise StopAsyncIteration
        return val

# 异步迭代器必须由协程函数中执行            
async def func():
    obj = Reader()
    async for i in obj:
        print(i)

# asyncio.run执行协程函数
asyncio.run(func())

十四、异步上下文管理器

此种对象通过定义 aenter() 和 aexit()方法来对 async_with 语句中的环境进行控制

import asyncio
import time

class AsyncContextManager:
    def __init__(self):
        self.conn = None
    
    async def do_somethins(self):
        # 异步操作数据库
        return '嘿嘿操作好了!'
    
    async def __aenter__(self):
        # 异步连接数据库
        self.conn = await asyncio.sleep(1)
        return self
    
    async def __aexit__(self, exc_type, exc, tb):
        # 异步关闭数据库连接
        await asyncio.sleep(1)

async def func():
    async with AsyncContextManager() as f:
        result = await f.do_somethins()
        print(result)

if __name__ == '__main__':
    asyncio.run(func())

十五、uvloop

uvloop是asyncio事件循环的替代方案。它事件循环效率>默认asyncio的事件循环,性能更高
安装:pip3 install uvloop

fastapi是很火的一个支持协程编程的python web框架,它使用率asgi unicorn,而unicorn内部使用的就是uvloop

import asyncio
import uvloop
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())

# 编写 asyncio 的代码,与之前的代码一致

# 内部的事件循环自动转化为uvlooop
asyncio.run()

十六、 异步操作Redis

在使用python代码操作redis时,连接/操作/断开都是网络IO
安装:pip3 install aioredis

16.1 单任务

import asyncio
import aioredis

async def execute(url):
    print('建立连接: ', url)
    
    # 网络IO操作:创建redis连接
    redis = await aioredis.from_url(
        url=url,
        max_connections=10,
        decode_responses=True,
        db=4
    )

    await redis.set('my-key', 'my-value')
    await redis.mset({'key:1': 'value1', 'key:2': 'value2'})

    val1 = await redis.get('my-key')
    val2 = await redis.get('key:1')

    await redis.close()
    print(val1, val2)

url = 'redis://127.0.0.1:6379'
asyncio.run(execute(url))

16.2 多任务

import asyncio
import aioredis

async def execute(url):
    print('建立连接: ', url)
    
    # 网络IO操作:创建redis连接
    redis = await aioredis.from_url(
        url=url,
        max_connections=10,
        decode_responses=True,
        db=4
    )

    await redis.set('my-key', 'my-value')
    await redis.mset({'key:1': 'value1', 'key:2': 'value2'})

    val1 = await redis.get('my-key')
    val2 = await redis.get('key:1')

    await redis.close()    
    print(val1, val2)

task_list = [
    execute('redis://127.0.0.1:6379'),
    execute('redis://127.0.0.1:6379'),
]
asyncio.run(asyncio.wait(task_list))

十七、异步操作MySQL

安装:pip3 install aiomysql

17.1 单任务

import asyncio
import aiomysql

async def test_example():
    conn = await aiomysql.connect(host='127.0.0.1', port=3306,
                                  user='root', password='heihei',
                                  db='devops_dev')

    cursor = await conn.cursor()
    await cursor.execute("SELECT instance_id, name FROM ops_host limit 10")
    r = await cursor.fetchall()
    await cursor.close()
    conn.close()
    print(r)

asyncio.run(test_example())

17.2 多任务

import asyncio
import aiomysql

async def test_example():
    conn = await aiomysql.connect(host='127.0.0.1', port=3306,
                                  user='root', password='heihei',
                                  db='devops_dev')

    cursor = await conn.cursor()
    await cursor.execute("SELECT instance_id, name FROM ops_host limit 10")
    r = await cursor.fetchall()
    await cursor.close()
    conn.close()
    print(r)

task_list = [
    test_example(),
    test_example()
]

asyncio.run(asyncio.wait(task_list))

十八、FastAPI

pip3 install fastapi
pip3 install uvicorn (asgi 支持异步的wsgi,内部基于uvloop)
uvicorn bubu:app --reload

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

redis_pool = aioredis.ConnectionPool.from_url(
    url='redis://127.0.0.1:6379',
    max_connections=10,
    decode_responses=True,
    db=4
)
redis = aioredis.Redis(connection_pool=redis_pool)

app = FastAPI()

@app.get("/")
async def root():
    await asyncio.sleep(3)
    return {"message": "Hello World"}

@app.get('/red')
async def red():
    print('嘿嘿我请求来了')

    await asyncio.sleep(3)
    await redis.execute_command('set', 'aa', 'bb')
    val = await redis.execute_command('get', 'aa')
    return val

if __name__ == '__main__':
    uvicorn.run('bubu:app', host='127.0.0.1', port=8888, log_level='info')

十九、爬虫

import aiohttp
import asyncio

async def fetch(url):
    print('发送请求')
    async with aiohttp.ClientSession() as session:
        async with session.get(url, verify_ssl=False) as response:
            result = await response.text()
            print('result: ', result)

async def main():
    url_list = [
        'https://python.org',
        'https://www.baidu.com',
        'https://www.taobao.com'
    ]
    tasks = [fetch(url) for url in url_list]
    await asyncio.wait(tasks)

if __name__ == '__main__':
    asyncio.run(main())
import aiohttp
import asyncio

async def fetch(session, url):
    print('发送请求')
    async with session.get(url, verify_ssl=False) as response:
        result = await response.text()
        print('result: ', result)
        return result

async def main():
    async with aiohttp.ClientSession() as session:
        url_list = [
            'https://python.org',
            'https://www.baidu.com',
            'https://www.taobao.com'
        ]
        tasks = [fetch(session, url) for url in url_list]
        done, pending = await asyncio.wait(tasks)
        print('执行结果: ', done)

if __name__ == '__main__':
    asyncio.run(main())
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值