Python asyncio异步编程 学习笔记

跟着武沛齐老师教程做的学习笔记
主要参考https://pythonav.com/wiki/detail/6/91/
课程链接(《asyncio异步编程》,讲师:武沛齐老师)


1 协程

1.1 简介

协程,又称微线程,英文名Coroutine。

协程是一种用户态内的上下文切换技术。简而言之,就是通过一个线程实现代码块相互切换执行(协程可以利用IO等待的时间去执行一些其他的代码,从而提升代码执行效率。)

协程是一种比线程更加轻量级的存在,正如一个进程可以拥有多个线程一样,一个线程可以拥有多个协程。
引自:https://www.jianshu.com/p/6dde7f92951e

在这里插入图片描述
协程的执行最终靠的还是线程,协程切换非常快,成本低,一般占用栈大小远小于线程(协程 KB 级别,线程 MB 级别),所以可以开更多的协程。

因此简言之, 进程可以包含多个线程,多个线程共享进程的资源,因此线程比进程更轻量;而协程的本质是一个函数,一个线程可以包含多个协程,协程比线程更轻量。

例如

def func1():
    print(1)
    ...
    print(2)
def func2():
    print(3)
    ...
    print(4)
    
func1()
func2()

一般的执行顺序是先执行 fun1 ,完了之后在执行 fun2 ,先后会输出:1、2、3、4

例如这种串行的方式

在这里插入图片描述
来源:网络


如果是协程就会在代码之间进行切换,比如由协程执行时,在执行 fun1 的过程中,可以随时中断,去执行 fun2 ,fun2 也可能在执行过程中中断再去执行 fun2,来回切换执行 ,最终可能输入:1、3、2、4

这就是这句话的意思

协程是一种用户态内的上下文切换技术。简而言之,其实就是通过一个线程实现代码块相互切换执行。

上述例子看起来 fun1 、fun2 的执行有点像多线程,但协程的特点在于是一个线程执行,即协程是一个线程执行,因此协程具有极高的执行效率。因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。

1.2 在Python中实现协程方式

  • greenlet,是一个第三方模块,用于实现协程代码(Gevent协程就是基于greenlet实现)

  • yield,生成器,借助生成器的特点也可以实现协程代码。

  • asyncio,在Python3.4中引入的模块用于编写协程代码。

  • async & awiat,在Python3.5中引入的两个关键字,结合asyncio模块可以更方便的编写协程代码【官方推荐】。

1.2.1 greenlet

安装

pip install -i http://pypi.douban.com/simple --trusted-host pypi.douban.com greenlet

下面由一个线程在执行代码的时候,在多个函数之间来回切换执行

from greenlet import greenlet

def func1():
    print(1)        # 第1步:输出 1
    gr2.switch()    # 第3步:切换到 func2 函数
    print(2)        # 第6步:输出 2
    gr2.switch()    # 第7步:切换到 func2 函数,从上一次执行的位置继续向后执行

def func2():
    print(3)        # 第4步:输出 3
    gr1.switch()    # 第5步:切换到 func1 函数,从上一次执行的位置继续向后执行
    print(4)        # 第8步:输出 4

gr1 = greenlet(func1)
gr2 = greenlet(func2)

gr1.switch() # 第1步:去执行 func1 函数

在这里插入图片描述

1.2.2 yield

生成器,借助生成器的特点也可以实现协程代码。

def func1():
    yield 1
    yield from func2()
    yield 2
    
def func2():
    yield 3
    yield 4
    
f1 = func1()
# 每循环一次相当于执行func1中一次yield
for item in f1:
    print(item)

在这里插入图片描述

1.2.3 asyncio

asyncio模块是专门用于实现协程的

加了 @asyncio.coroutine 装饰器,由普通函数变为协程函数,执行时必须通过

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

才能正常的执行


import asyncio

# 装饰器 coroutine 协程函数
@asyncio.coroutine
def func1():
    print(1)
    yield from asyncio.sleep(1)  # 遇到IO耗时操作,在等待的过程中,自动化切换到tasks中的其他任务
    print(2)
    
    
    
@asyncio.coroutine
def func2():
    print(3)
    yield from asyncio.sleep(2) # 遇到IO耗时操作,在等待的过程中,自动化切换到tasks中的其他任务
    print(4)
 

    
# task 任务:一个协程对象就是一个原生可以挂起的函数,任务则是对协程进一步封装,其中包含了任务的各种状态    
# 创建一个task
# 协程对象不能直接运行,在注册事件循环的时候,
# 其实是run_until_complete方法将协程包装成为了一个任务(task)对象. 
# task对象是Future类的子类,保存了协程运行后的状态,用于未来获取协程的结果
    
tasks = [
    asyncio.ensure_future( func1() ),
    asyncio.ensure_future( func2() )
]

# event_loop 事件循环:程序开启一个无限循环,把一些函数注册到事件循环上,当满足事件发生的时候,调用相应的协程函数
# asyncio.get_event_loop:创建一个事件循环,
# 然后使用run_until_complete将协程注册到事件循环,并启动事件循环

# 
#  创建一个事件loop
loop = asyncio.get_event_loop()
# 把fun1 和fun2 打包,会同时执行这两个函数
# 将协程加入到事件循环loop
# 创建task后,在task加入事件循环之前为pending状态,当完成后,状态为finished
loop.run_until_complete(asyncio.wait(tasks))

在这里插入图片描述

asyncio 遇到IO耗时操作(有网络就有IO,比如通过爬虫访问网址下载图片),在等待的过程中,自动化切换到tasks中的其他任务

大大提高了执行效率

1.2.4 async & awit

本质和上面差不多,只是为了让代码更加简洁,进入async & awit这两个关键字

@asyncio.coroutine 装饰器就会被移除,推荐使用async & awit 关键字实现协程代码。

@asyncio.coroutine ==》 async

yield from ==》 await

import asyncio

async def func1():
    print(1)
    await asyncio.sleep(2)
    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))

因此,根据以上所有,可以把协程理解为在一个线程中如果遇到IO等待时间,不会等,会利用空闲的时间干点其他事。

压榨剩余价值,不让它闲着

1.2.5 普通爬虫与协程爬虫

插曲:因为async & awit是Python3.8之后的,我使用的python版本是3.7,又不想升级,所以单独又装了一个3.9,在IDLE上运行这部分代码

1.2.5.1 普通爬虫(同步:按照顺序逐一排队执行)
import requests
import time

def download_image(url):
    print("开始下载:",url)
    # 发送网络请求,下载图片
    response = requests.get(url)
    print("下载完成")
    # 图片保存到本地文件
    file_name = url.rsplit('_')[-1]
    with open(file_name, mode='wb') as file_object:
        file_object.write(response.content)


if __name__ == '__main__':
    url_list = [
        'https://www3.autoimg.cn/newsdfs/g26/M02/35/A9/120x90_0_autohomecar__ChsEe12AXQ6AOOH_AAFocMs8nzU621.jpg',
        'https://www2.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'
    ]
    start_time=time.time()
    for item in url_list:
        download_image(item)
    end_time=time.time()
    print('耗时:',end_time-start_time)

在这里插入图片描述

1.2.5.2 基于协程的异步编程实现(异步:遇到IO请求自动切换去发送其他任务请求)

使用 aiohttp 获取

import aiohttp
import asyncio


async def fetch(session, url):
    print("发送请求:", url)
    # 发送请求,IO有等待的时间,不会等待,继续发送第二个请求
    # 获取响应 response
    async with session.get(url, verify_ssl=False) as response:
        content = await response.content.read()

        # 图片保存到本地文件
        file_name = url.rsplit('_')[-1]
        with open(file_name, mode='wb') as file_object:
            file_object.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://www2.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]
        await asyncio.wait(tasks)


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

先发送一个请求,不等返回结果,又发送下一个请求…

执行效率显著提高
在这里插入图片描述

2 异步编程

2.1 事件循环

理解成一个死循环,去检测并执行任务列表中的任务。


当我们定义了一个任务列表时,任务列表中的每个任务都会有一个状态,比如说可执行,等待执行(即IO操作,程序会忽略掉此任务,等它的状态变为可执行时才执行此任务),执行完成等。

事件循环会先去任务列表中检查任务的状态,并将状态为可执行和已完成的任务返回,然后再去循环返回的可执行任务列表执行任务,循环已完成任务列表将已完成任务从任务列表中移除,然后进行下一次的检测,直至任务列表为空是终止循环。
(引自:https://blog.csdn.net/weixin_46297209/article/details/111464854)

# 伪代码

任务列表 = [ 任务1, 任务2, 任务3,... ]

while True:

    可执行的任务列表,已完成的任务列表 = 去任务列表中检查所有的任务,将'可执行''已完成'的任务返回
    
    for 就绪任务 in 已准备就绪的任务列表:
        执行已就绪的任务
        
    for 已完成的任务 in 已完成的任务列表:
        在任务列表中移除 已完成的任务
        
    如果 任务列表 中的任务都已完成,则终止循环

在异步编程中,事件循环的获取与创建

import asyncio
# 生成 or 获取 一个事件循环
loop = asynico.get_event_loop()

# 将任务加入到任务列表,即事件循环中,去检测任务的状态,是否是可运行,是否是IO
loop.run_until_complete(tasks)

2.2 async关键字(普通函数==>协程函数)

协程函数: async def

async def func1():
    print(1)
    await asyncio.sleep(2)
    print(2)

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

re = func1()

# re为协程对象

注意:与普通函数不同,执行协程函数创建协程对象时,协程函数内部代码不会执行,只是获取一个协程对象,必须与事件循环配合使用,即将协程对象交给事件循环处理。

如果想要运行协程函数内部代码,必须将它放入事件循环中

import asyncio

async def fun1():
    print('哈哈哈哈哈')
    
# 生成 or 获取 一个事件循环
loop = asyncio.get_event_loop()
# 将任务加入到任务列表,即事件循环中,去检测任务的状态,是否是可运行,是否是IO
loop.run_until_complete(fun1())

上述写法还是太麻烦

python3.7 之后,有了更简便的写法

import asyncio

async def fun1():
    print('哈哈哈哈哈')
    
# 调用协程函数
asyncio.run(fun1()) 

2.3 await关键字(等)

​ await + 可等待对象(协程对象,Future,Task对象),即等待IO对象

遇到 await关键字 即进入 IO等待(比如说向服务器发起请求等待响应返回内容),如果有其他的任务,此时不会等待而是会切换到其他任务,如果这个等待完成,则会继续执行下面的操作

注意:await 后面只能跟特定的对象

import asyncio

async def myfun1():
    print("你猜~")
    await asyncio.sleep(2)
    print("这都猜不到,我是你的小可爱啊!")
    return "生气了"


async def myfun2():
    print("你是谁?")

    # 将await等待的对象赋值给response
	# 遇到 IO等待时,会切换到其他协程对象
	# await 等 == myfun1()有返回值了 才执行下面的代码
    response = await myfun1()
    print("你怎么了?\n", response)


asyncio.run(myfun2())

在这里插入图片描述

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

import asyncio

async def myfun1():
    print("你猜~")
    await asyncio.sleep(2)
    print("这都猜不到,我是你的小可爱啊!")
    return "生气了"


async def myfun2():
    print("你是谁?")

    response1 = await myfun1()
    print("你怎么了?\n", response1)

    print('-'*10)
    
    response2 = await myfun1()
    print("你怎么了?\n", response2)


asyncio.run(myfun2())

注意:这里任务列表中只有一个任务,在等待 response1 = await myfun1() 时,不会执行其他的,这里只有一个任务
在这里插入图片描述

2.4 Tasks对象

在事件循环中添加多个任务,即向任务列表中添加任务,用于并发调度协程。

通过 asyncio.create_task(协程对象) 的方式创建Task对象,这样可以让协程加入事件循环中等待被调度执行。

import asyncio

async def func():
    print("start")
    await asyncio.sleep(1)
    print("end")
    return "返回值"


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

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

    # 创建Task对象,将当前执行func函数任务添加到事件循环
    # 此时任务列表中有三个任务,分别为main,func,func
    task2 = asyncio.create_task(func())
    print("main结束")

    # main任务遇到了await task1,意味着mian任务要等待task1执行完并且有了返回值之后才能再往下执行
    # 忽略等待状态的任务,切换到task1,即执行func函数,
    # 输出start后进入等待状态,进入task2任务,即再次执行func函数
    # 输出start后进入等待状态,进入task1任务,输出end,并返回函数值
    # task1任务结束,从任务列表中进行删除,任务列表中只剩下main任务以及执行了一半的task2任务
    # response1接受到返回值,main任务开始往下执行
    response1 = await task1
    # main任务遇到await task2,意味着main任务要等待task2执行完毕并且有了返回值之后再往下执行
    # 切换到task2任务,从上次等待的地方往下执行,输出end,并返回函数值
    # task2任务结束,response2接受返回值,main任务继续往下执行
    response2 = await task2

    # 打印task1和task2的返回值response1和response2
    print(response1, response2)


if __name__ == '__main__':
    # 将main函数放入到任务列表中
    asyncio.run(main())

代码:从概念到案例全面剖析协程

该博主分析的很好!
在这里插入图片描述


更常见的写法是把 task1 与 task2 …,这些任务放到一个列表中,

task1 = asyncio.create_task(func())
task2 = asyncio.create_task(func())


==》


tasks = [
        asyncio.create_task(func(),name='任务1'),
        asyncio.create_task(func(),name='任务2')
 ]

并且将

response1 = await task1
response2 = await task2

改写为

done, pending = await asyncio.wait(tasks, timeout=None)

import asyncio

async def func():
    print("start")
    await asyncio.sleep(1)
    print("end")
    return "这是func的返回值"

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

    # 将任务放入到一个列表中
    tasks = [
        asyncio.create_task(func(),name='任务1'),
        asyncio.create_task(func(),name='任务2')
    ]

    print("main结束")

    done, pending = await asyncio.wait(tasks, timeout=None)

    print(done)
    print('-'*20)
    print(pending)


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

在这里插入图片描述

这里 await 后面不能跟一个列表,需要用 asyncio.wait 将它转换为一个协程对象,等待这个列表中的任务执行完毕

最终 await 返回一个包含(done, pending)的元组,done表示已完成的任务集合,集合元素是 tasks 中的所有任务的返回值pending表示未完成的任务列表

timeout是指过时时间,如果为 timeout=2 时,表示最多等2秒,如果2秒没有返回值,就不再接收返回值;如果为None时,表示一直等待返回值。

注:

  • 只有当给wait()传入timeout参数时才有可能产生pending列表。
  • 通过wait()返回的结果集是按照事件循环中的任务完成顺序排列的,所以其往往和原始任务顺序不同。

上述例子是把所有任务 tasks 写在了协程函数中,如果要将 tasks 写在协程函数外,可以这样写

import asyncio

async def func():
    print("start")
    await asyncio.sleep(1)
    print("end")
    return "这是func的返回值"


# 如果在这里直接添加任务则会报错,因为我们还未创建事件循环,所以无法添加任务
# 我们只需将协程对象放入列表中即可
# tasks = [
#     asyncio.create_task(func()),
#     asyncio.create_task(func())
# ]

tasks = [
    func(),
    func()
]

# 这里程序会自动为我们先创建事件循环,然后再创建任务
done, pending = asyncio.run(asyncio.wait(tasks))

print(done)

在这里插入图片描述


2.5 asyncio.Future对象

一般不会手动设置,仅供我们理解await的处理过程,实际中不会使用。

Future是一个更偏底层的接口,用于等待异步处理的结果。

await 等待结果本质是由 Future 创建的

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

import asyncio

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

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

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

asyncio.run(main()) # asyncio.run已经创建好事件循环

Future对象通常用来让程序进入一个等待状态。Task对象是无法实现这个操作的,因为我们在创建Task对象时通常会绑定一个协程对象,而在执行任务即协程函数时,函数内没有返回值或没有内容都将自动返回一个None,使函数不再继续等待。(https://blog.csdn.net/weixin_46297209/article/details/111464854)

2.6 concurrent.futures.Future对象

concurrent.futures.Future对象 与 asyncio.Future对象 没有关系

使用场景:比如说我们在一个项目中导入的第三方包不支持async的异步编程时,就需要将协程与进程池、线程池结合使用。

3 案例 asyncio + 不支持异步的模块(爬虫,如requests)

上面说到

协程与进程池和线程池交叉使用
使用场景:比如说我们在一个项目中导入的第三方包不支持async的异步编程时,就需要将协程与进程池、线程池结合使用。

例如

requests模块就默认不支持异步操作(不支持async关键字),所以需要使用线程池来配合实现了,即使用异步与非异步结合的方式进行爬取

import asyncio
import requests
import os

os.chdir(r'C:\Users\Administrator\Desktop')

async def download_image(url):
  
    print('开始下载。')

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

    # requests模块默认不支持异步操作,可以配合线程池实现异步操作
    # 会创建多个线程
    # 创建一个线程池,将函数和参数扔进线程池中进行请求
    # 参数None表示使用默认的线程池。
    fut = loop.run_in_executor(None, requests.get, url)

    
    # 此时遇到IO操作,不会等待,继续切换下一个任务,即将另一个请求扔进线程池中
    resp = await fut
    
    print('下载完成。')

    # 保存
    file_name = url.rsplit('/')[-1]
    with open(file_name, mode='wb') as file_obj:
        file_obj.write(resp.content)


if __name__ == '__main__':
  
    url_list = [
        'https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=2108399405,646348254&fm=26&gp=0.jpg',
        'https://ss1.bdstatic.com/70cFvXSh_Q1YnxGkpoWK1HF6hhy/it/u=1091405991,859863778&fm=26&gp=0.jpg',
        'https://ss2.bdstatic.com/70cFvnSh_Q1YnxGkpoWK1HF6hhy/it/u=3205720277,4209513487&fm=26&gp=0.jpg'
    ]
    
    #创建任务列表
    task_list = [download_image(each_url) for each_url in url_list]

    # 获取当前的事件循环
    #loop = asyncio.get_event_loop()
    #loop.run_until_complete(asyncio.wait(task_list))

    asyncio.run(asyncio.wait(task_list))

在这里插入图片描述

4 异步迭代器

异步迭代器:实现了方法__aiter__()和__anext__()的对象,aiter__必须返回一个awaitable对象,async for会处理异步迭代器的方法__anext()所返回的可等待对象,直到其引发了一个StopAsyncIteration异常。

5 异步上下文管理器

通过定义方法__aenter__()和__aexit__()对async with语句中的环境进行控制。

6 事件循环升级 uvloop

uvloop是asyncio中的事件循环的替代方案,大幅度提高事件循环效率和性能。

7 爬虫

在编写爬虫应用时,需要通过网络IO去请求目标数据,这种情况适合使用异步编程来提升性能,接下来我们使用支持异步编程的aiohttp模块来实现。

安装aiohttp模块

pip install aiohttp
import aiohttp
import asyncio


async def fetch(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',
        ]
        tasks = [asyncio.create_task(fetch(session, url)) for url in url_list]
        await asyncio.wait(tasks)

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

参考:
https://pythonav.com/wiki/detail/6/91/
从概念到案例全面剖析协程
2 异步编程 asyncio模块

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值