了解python协程与异步编程

一、了解下并发和并行的区别

image

二、了解下python中线程和协程的区别

image

举个例子
假设有1个洗衣房,里面有10台洗衣机,有一个洗衣工在负责这10台洗衣机。
那么洗衣房就相当于1个进程,洗衣工就相当1个线程。
如果有10个洗衣工,就相当于10个线程,1个进程是可以开多线程的。这就是多线程!

那么协程呢?
先不急。大家都知道,洗衣机洗衣服是需要等待时间的,如果10个洗衣工,1人负责1台洗衣机,这样效率肯定会提高,但是不觉得浪费资源吗?
明明1 个人能做的事,却要10个人来做。只是把衣服放进去,打开开关,就没事做了,等衣服洗好再拿出来就可以了。
就算很多人来洗衣服,1个人也足以应付了,开好第一台洗衣机,在等待的时候去开第二台洗衣机,再开第三台,……直到有衣服洗好了,就回来把衣服取出来,
接着再取另一台的(哪台洗好先就取哪台,所以协程是无序的)。这就是计算机的协程!洗衣机就是执行的方法。”

三、协程的概念

协程(Coroutine),也可以被称为微线程,是一种用户态内的上下文切换技术。简而言之,其实就是通过一个线程实现代码块相互切换执行。例如:

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

上述代码是普通的函数定义和执行,按流程分别执行两个函数中的代码,并先后会输出:1、2、3、4。但如果介入协程技术那么就可以实现函数见代码切换执行,最终输入:1、3、2、4 。

在Python中有多种方式可以实现协程,例如:

  • greenlet,是一个第三方模块,用于实现协程代码(Gevent协程就是基于greenlet实现)
  • yield,生成器,借助生成器的特点也可以实现协程代码。
  • asyncio,在Python3.4中引入的模块用于编写协程代码。
  • async & awiat,在Python3.5中引入的两个关键字,结合asyncio模块可以更方便的编写协程代码。****

下面一起来熟悉几个概念

1协程函数

普通函数前加async,就是一个协程函数,如下

async def async_fun():
    print('我是一个异步函数')

print(async_fun())
af.send(None)
-------执行结果--------
<coroutine object async_fun at 0x0000026F8E8CD940>
E:/bf_20220725//demo.py:533: RuntimeWarning: coroutine 'async_fun' was never awaited
  print(async_fun())
RuntimeWarning: Enable tracemalloc to get the object allocation traceback
我是一个异步函数

注意:异步函数不能直接被执行,返回结果是一个协程对象,可以想生成器一样去使用.send()方法调用

2协程对象

异步函数调用后返回的就是一个协程对象,见1、异步函数

3事件循环

事件循环是每个 asyncio 应用的核心。 事件循环会运行异步任务和回调,执行网络 IO 操作,以及运行子进程

事件循环,可以把他当做是一个while循环,这个while循环在周期性的运行并执行一些任务,在特定条件下终止循环。

# 伪代码
任务列表 = [ 任务1, 任务2, 任务3,... ]
while True:
    可执行的任务列表,已完成的任务列表 = 去任务列表中检查所有的任务,将'可执行'和'已完成'的任务返回
    for 就绪任务 in 已准备就绪的任务列表:
        执行已就绪的任务
    for 已完成的任务 in 已完成的任务列表:
        在任务列表中移除 已完成的任务
    如果 任务列表 中的任务都已完成,则终止循环

在编写程序时候可以通过如下代码来获取和创建事件循环。

import asyncio
loop = asyncio.get_event_loop()

四 异步编程asyncio

1运行一个协程的方法

  • 使用asyncio.run() 函数用来运行最高层级的入口点 "main()" 函数

程序中,如果想要执行协程函数的内部代码,需要 事件循环 和 协程对象 配合才能实现,如:

import asyncio
async def func():
    print("协程内部代码")
# 调用协程函数,返回一个协程对象。
result = func()
# 方式一
# loop = asyncio.get_event_loop() # 创建一个事件循环
# loop.run_until_complete(result) # 将协程当做任务提交到事件循环的任务列表中,协程执行完成之后终止。
# 方式二
# 本质上方式一是一样的,内部先 创建事件循环 然后执行 run_until_complete,一个简便的写法。
# asyncio.run 函数在 Python 3.7 中加入 asyncio 模块,
asyncio.run(result)

这个过程可以简单理解为:将协程当做任务添加到 事件循环 的任务列表,然后事件循环检测列表中的协程是否 已准备就绪(默认可理解为就绪状态),如果准备就绪则执行其内部代码。(当前事件循环中只有result一个TASK)

  • 使用await 等待一个协程

以下代码段会在等待 1 秒后打印 "hello",然后 再次 等待 2 秒后打印 "world"

import asyncio
import time

async def say_after(delay, what):
    await asyncio.sleep(delay)
    print(what)

async def main():
    print(f"started at {time.strftime('%X')}")

    await say_after(1, 'hello')
    await say_after(2, 'world')

    print(f"finished at {time.strftime('%X')}")

asyncio.run(main())
  • asyncio.create_task() 函数用来并发运行作为 asyncio 任务 的多个协程

async def main():
    task1 = asyncio.create_task(
        say_after(1, 'hello'))

    task2 = asyncio.create_task(
        say_after(2, 'world'))

    print(f"started at {time.strftime('%X')}")

    # Wait until both tasks are completed (should take
    # around 2 seconds.)
    await task1
    await task2

    print(f"finished at {time.strftime('%X')}")

2.await

await是一个只能在协程函数中使用的关键字,用于遇到IO操作时挂起 当前协程(任务),当前协程(任务)挂起过程中 事件循环可以去执行其他的协程(任务),当前协程IO处理完成时,可以再次切换回来执行await之后的代码。

**await 后面只能跟可以被等待的对象

2.1可等待对象**

  • 协程

    Python 协程属于 可等待 对象,因此可以在其他协程中被等待:

  • 任务

    任务 被用来“并行的”调度协程
    当一个协程通过 asyncio.create_task() 等函数被封装为一个 任务,该协程会被自动调度执行,即会自动在事件循环中创建一个task

  • future

    Future 是一种特殊的 低层级 可等待对象,表示一个异步操作的 最终结果。
    当一个 Future 对象 被等待,这意味着协程将保持等待直到该 Future 对象在其他地方操作完毕

2.2 可等到对象案例演示

实例1 等待对象是一个协程:

import asyncio

async def nested():
    return 42

async def main():
    print(await nested())  # will print "42".

asyncio.run(main())

总结:当await后面跟着一个协程对象对象时,直接执行协程内部的代码,当遇到IO操作挂起当前协程(任务),等IO操作完成之后再继续往下执行。当前协程挂起时,事件循环可以去执行其他协程(任务)。

实例2 等待对象是一个task

import asyncio

async def nested():
    return 42

async def main():
    task = asyncio.create_task(nested())
    await task

asyncio.run(main())

总结:asyncio.create_task(协程对象)的方式创建Task对象,本质上是将协程对象封装成task对象,并将协程立即加入事件循环,同时追踪协程的状态

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


实例3:等待对象是一个future(一般不会用到)

import asyncio
async def set_after(fut):
    await asyncio.sleep(2)
    fut.set_result("666")
async def main():
    # 获取当前事件循环
    loop = asyncio.get_running_loop()
    # 创建一个任务(Future对象),没绑定任何行为,则这个任务永远不知道什么时候结束。
    fut = loop.create_future()
    # 创建一个任务(Task对象),绑定了set_after函数,函数内部在2s之后,会给fut赋值。
    # 即手动设置future任务的最终结果,那么fut就可以结束了。
    await loop.create_task(set_after(fut))
    # 等待 Future对象获取 最终结果,否则一直等下去
    data = await fut
    print(data)
asyncio.run(main())

**总结:Future对象本身函数进行绑定,所以想要让事件循环获取Future的结果,则需要手动设置。而Task对象继承了Future对象,其实就对Future进行扩展,他可以实现在对应绑定的函数执行完成之后,自动执行set_result,从而实现自动结束。

2.3 接收异步函数返回值

编程过程中,如果我们想接收异步函数的返回值使 await + 协程函数/任务/future

五 实战案例演示

协程main函数定义两个await(后跟协程对象)

mport asyncio
import time

async def say_after(delay, what):
    await asyncio.sleep(delay)
    print(what)

async def main():
    print(f"started at {time.strftime('%X')}")

    await say_after(1, 'hello')
    await say_after(2, 'world')

    print(f"finished at {time.strftime('%X')}")

asyncio.run(main())

总结:事件循环中始终只有main一个任务,所以遇到IO也无法进行切换,其实还是同步执行

改进上面案例,协程main函数定义两个await(后跟任务)

async def main():
    task1 = asyncio.create_task(
        say_after(1, 'hello'))

    task2 = asyncio.create_task(
        say_after(2, 'world'))

    print(f"started at {time.strftime('%X')}")
    await task1
    await task2

    print(f"finished at {time.strftime('%X')}")

总结:asyncio.create_task()可以将协程对象加入到事件循环列表中,例子创建了两个任务,当执行task1时遇到IO堵塞,事件循环会调度执行task2,从而达到并发的效果

假如我们有很多个任务,总不能都像这样await 任务这样去写吧,这样会很傻,asyncio提供了wait和gather这两个方法假如我们有很多个任务,总不能都像这样await 任务这样去写吧,这样会很傻,asyncio提供了wait和gather这两个方法

1 asyncio.gather(*aws, return_exceptions=False)

如果所有可等待对象都成功完成,结果将是一个由所有返回值聚合而成的列表。结果值的顺序与 aws 中可等待对象的顺序一致

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="n1"),
        asyncio.create_task(func(), name="n2")
    ]
    print("main结束")
    result = await asyncio.gather(*task_list)
    print(result)

asyncio.run(main())
-----返回结果------
main开始
main结束
1
1
2
2
['返回值', '返回值']

2asyncio.wait(aws, *, timeout=None, return_when=ALL_COMPLETED)

返回两个 Task/Future 集合: (done, pending)。

用法:
done, pending = await asyncio.wait(aws)
如指定 timeout (float 或 int 类型) 则它将被用于控制返回之前等待的最长秒数。
请注意此函数不会引发 asyncio.TimeoutError。当超时发生时,未完成的 Future 或 Task 将在指定秒数后被返回。

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="n1"),
        asyncio.create_task(func(), name="n2")
    ]
    print("main结束")
    done, pending = await asyncio.wait(task_list, timeout=None)
    print(done, pending)

asyncio.run(main())
---------执行结果---------
main开始
main结束
1
1
2
2
{<Task finished name='n1' coro=<func() done, defined at E:\demo.py:574> result='返回值'>, 
<Task finished name='n2' coro=<func() done, defined at E:\demo.py:574> result='返回值'>} set()

3 使用协程对接口实现高并发案例:

image

可以看到500和请求在3ms内全部处理完成了,效果还是不错的!

参考文档:https://pythonav.com/wiki/detail/6/91/#3.2.2 await
参考文档:https://docs.python.org/zh-cn/3/library/asyncio-task.html#coroutines
参考视频:https://www.bilibili.com/video/BV1Xq4y1D7Fs?spm_id_from=333.337.search-card.all.click&vd_source=a651f08d17caa1088efe9aca8af3670e
参考视频:https://www.bilibili.com/video/BV1oa411b7c9/?spm_id_from=333.788
参考视频:https://www.bilibili.com/video/BV1dD4y127bD?p=9&spm_id_from=pageDriver&vd_source=a651f08d17caa1088efe9aca8af3670e

posted @ 2022-08-02 16:17  上官夏洛特  阅读(407)  评论(0编辑  收藏  举报