Python 异步 asyncawait(进阶详解)

#Python 异步 async/await(进阶详解)

CPU的时间观

如果电脑的主频为2.6G,也就是每秒可以执行2.6*10^9个指令,每个指令只需要0.38ns。 假设一个指令时间比作人类能够感知的一秒钟来对比数据,如下图: (借用一张网络download的CPU时间观的对比图)

在这里插入图片描述

举个常见的例子 1Gbps网络上传输2KB数据,真实延迟20微秒,CPU感觉过了14.4小时!!!

  • CPU是计算机的核心,如果利用率低,不仅浪费资源,而且程序执行效率低下,需要耗费更长的时间(这是最应该关注的点)- 如果不提高程序执行效率,等同于“谋财害命

I/O(异步的瓶颈)

  • CPU的时间观中可以看出除了执行指令一级缓存,对CPU来说是可接受的,其他操作都很慢很慢很慢很慢很慢很慢(特别是从上下文切换开始)!!!- 回顾一下大学期间,被老师强调无数次的的冯·诺依曼结构- 数学家冯·诺依曼提出了计算机制造的三个基本原则,即采用二进制逻辑、程序存储执行以及计算机由五个部分组成(运算器、控制器、存储器、输入设备、输出设备),这套理论被称为冯·诺依曼体系结构
  • 冯·诺依曼体系结构得出,运算器控制器主要集成在CPU上的,其余全是I/O,包括磁盘读写,网卡读写等等。- 异步可以提高程序效率,但是I/O依旧是最大的瓶颈!- 因此I/O是程序效率的最大瓶颈!!!!!- 因此I/O是程序效率的最大瓶颈!!!!!- 因此I/O是程序效率的最大瓶颈!!!!! (没有卡,重要的事情说三遍!)- 世界上最大的计算机程序,莫过于因特网,因此网络I/O(CPU的时间观)成为了I/O的最大瓶颈!- 除了宕机,网络I/O是最慢的 最慢的 最慢的!因此诸多异步框架基本都是处理网络I/O的。

  • 后面介绍使用协程去处理异步程序,尽可能的利用I/0操作时的等待时间去执行其他动作,提高程序执行效率。

基础概念

如果基础概念已经了解,请直接跳过本节。

进程/线程

  • 进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。- 线程(thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。在Unix System V及SunOS中也被称为轻量进程(lightweight processes),但轻量进程更多指内核线程(kernel thread),而把用户线程(user thread)称为线程。- 一个进程可以有很多线程,每条线程并行执行不同的任务。

阻塞/非阻塞

  • 阻塞和非阻塞指的是调用者(程序)在等待返回结果(或输入)时的状态。- 阻塞时,在调用结果返回前,当前线程会被挂起,并在得到结果之后返回。- 非阻塞时,如果不能立刻得到结果,则该调用者不会阻塞当前线程。因此对应非阻塞的情况,调用者需要定时轮询查看处理状态。

并发/并行

  • 并发:在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机上运行。- 并行:在操作系统中是指,一组程序按独立异步的速度执行,无论从微观还是宏观,程序都是一起执行的。- 对比地讲,并发是:在同一个时间段内,两个或多个程序执行,有时间上的重叠(宏观上是同时,微观上仍是顺序执行)

CPU调度策略

  • 在并发运行中,CPU需要在多个程序之间来回切换,因此会有一些切换的调度策略。
  • 先来先服务 - 时间片轮转调度- 优先级调度- 最短作业优先- 最高响应比优先- 多级反馈队列调度

同步/异步

  • 同步:在发出一个同步调用时,在没有得到结果之前,该调用就不返回。- 异步:在发出一个异步调用后,调用者不会立刻得到结果,该调用就返回了。
  • 同步和阻塞的定义很像,但却是两个概念,同步不一定阻塞,在结果没有返回前,阻塞是指线程被挂起,这段程序不再执行,而同步调用是在运行状态,CPU还在执行这段程序。- 异步和非阻塞定义也很像,但也是两个概念,异步指在调用时不会立即得到结果,调用就返回了,线程可能阻塞,也可能不阻塞。而非阻塞是指调用的时候,线程一定不会进入非阻塞状态。

事件循环+回调

  • 所谓的事件循环,并非是一个真正意义上的循环,可以理解为一种定义,可以理解为是主线程不断的从事件队列里面取值/函数的过程,因为这一过程是不断发生的所以我们为了方便把这个过程叫事件循环。- 事件本身并没有循环的,循环的只是主线程取事件的动作。 软件模块之间总是存在着一定的接口,从调用方式上,可以把他们分为三类:同步调用、回调和异步调用。

    • 同步调用是一种阻塞式调用,调用方要等待对方执行完毕才返回,它是一种单向调用;
    • 回调是一种双向调用模式,也就是说,被调用方在接口被调用时也会调用对方的接口;
      详细概念:

      • 双向调用 通常指的是两个程序或组件之间相互调用的情况。这种情况通常在分布式系统中出现,例如微服务架构中的多个服务之间需要相互调用。双向调用还可以在网络编程中出现,例如客户端和服务器之间的通信需要相互调用。

      • 什么情况下出现双向调用 举例说明详细的使用场景

        • 双向调用通常发生在分布式系统中,其中两个或多个独立的进程需要相互通信并交换信息。以下是一些常见的使用场景:
        1. 实时通信:例如聊天应用程序,在这种情况下,客户端要发送消息,服务器必须接收并转发这些消息到其他客户端。同时,当其他客户端回复时,服务器也必须将消息发送回发送方。
        2. 远程过程调用(RPC):在这种情况下,客户端需要调用远程服务器上的一个函数或方法,并等待该函数返回结果。然而,服务器也可能需要访问客户端上的特定资源或状态,例如会话信息或身份验证令牌。
        3. 分布式计算:在这种情况下,多个计算节点需要协作完成某项任务。由于任务分配和数据分发通常需要双向通信,因此节点之间必须进行双向调用以确保任务能够正确执行。

        总之,双向调用是分布式系统中常见的通信模式,它可以使不同进程之间进行高效的信息交换和协作,从而实现更复杂的功能。

    • 异步调用是一种类似消息或事件的机制,不过它的调用方向刚好相反,接口的服务在收到某种讯息或发生某种事件时,会主动通知客户方(即调用客户方的接口)。

      • 异步调用使用场景

        • 异步调用通常在需要执行耗时操作的情况下使用,以避免阻塞主线程并提高程序效率。以下是一些异步调用的使用场景:

          1. 网络请求:在发起网络请求时,需要等待服务器响应,这可能需要花费较长时间,如果在主线程中执行此操作,界面将被阻塞,用户体验会受到影响。因此,通常使用异步调用来执行网络请求。

          2. 文件读写:对于大型文件的读取和写入,也需要耗费较长时间。在这种情况下,使用异步调用可以使主线程不被阻塞,并且可以同时进行其他操作。

          3. 定时器:定时器是一个周期性执行的任务。如果该任务是在主线程中执行,可能会影响用户体验。因此,使用异步调用来实现定时器功能是一个很好的选择。

          4. 并发编程:当多个任务同时执行时,为了避免出现死锁或竞争条件等问题,通常使用异步调用来实现并发编程。

          5. GUI编程:在GUI编程中,处理用户输入、绘制界面等操作需要在主线程中执行,如果同时还有其它耗时操作需要执行,那么就需要使用异步调用来实现并发执行多个任务。

          总之,任何需要执行耗时操作的场景都可以考虑使用异步调用来提高程序效率和用户体验。

    • 异步调用和双向调用有什么关系和差异

      • 回调和异步调用的关系非常紧密,通常我们使用回调来实现异步消息的注册,通过异步调用来实现消息的通知。

      • 详细说明的话异步调用和双向调用都是指不同的通信模式。

        异步调用是一种单向通信模式,即客户端发送请求后继续执行其他任务,服务端在处理完请求后再返回结果给客户端。在异步调用中,客户端无需等待服务端响应,从而可以提高系统的并发性和吞吐量。

        双向调用是一种双向通信模式,即客户端和服务端可以互相发送消息,在连接建立后随时进行通信。在双向调用中,客户端和服务端都可以主动发起请求、响应和推送消息。

        因此,异步调用和双向调用的差异在于其通信方向和模式不同。

协程(异步)

python中为了提高I/O效率,使用协程去处理异步程序,协程自动完善了上述的各种调度任务!

  • 协程通过实现一种用户级线程,可以在单个线程中实现多个任务之间的切换,从而提高程序的并发性能。协程可以利用不同的调度策略来自动完善各种调度任务,例如:

    1. 抢占式调度:使用时间片轮转算法将执行时间分配给每个协程,从而避免某个任务长时间阻塞导致其他任务无法执行。

    2. 协作式调度:每个协程执行完一定的任务后主动让出CPU,从而使得其他协程有机会执行,这样可以避免由于长时间运行而导致的任务饥饿问题。

    3. IO事件驱动调度:将IO操作与协程的yield操作结合起来,当一个IO操作完成时,唤醒等待该IO操作的协程继续执行,这种方式可以提高IO密集型应用的性能。

​ 通过以上不同的调度方式,协程可以自动适应不同的场景,满足各种调度任务的需求。

  • 进程和线程是计算机提供的,协程是程序员创造的,不存在于计算机中。 - 协程(Co-routine),也可称为微线程,或非抢占式的多任务子例程,一种用户态的上下文切换技术(通过一个线程实现代码块间的相互切换执行) - 意义:在一个线程(协程)中,遇到io等待时间,线程可以利用这个等待时间去做其他事情。

async/await

asyncio事件循环(python3.6)

  • 事件循环:去检索一个任务列表的所有任务,并执行所有未执行任务,直至所有任务执行完成。- 执行协程函数,必须使用事件循环。
# python 源码
import asyncio


async def func1():
    print('协程1')

async def func2():
    print('协程2')

# task可为列表,即任务列表
# task = func1()
task = [func1(), func2()]
# 创建事件循环
loop = asyncio.get_event_loop()
# 添加任务,直至所有任务执行完成
loop.run_until_complete(asyncio.wait(task))
# loop.run_until_complete() 是一个异步事件循环中的方法,它会一直运行事件循环,直到传入的可等待对象完成为止。这个方法会阻塞当前线程,直到事件循环停止或者发生错误。通常用于异步编程中等待协程执行完毕并获取结果。
#关闭事件循环
loop.close()
# 事件循环关闭后,再次调用loop,将不会再次执行。

asyncio事件循环(python3.7)

  • python3.7省略的手动创建事件循环,可直接用asyncio.run()去执行协程任务。
import asyncio

async def func1():
    print('协程1')

async def func2():
    print('协程2')

task = [func1(), func2()]
# python3.7引入的新特性,不用手动创建事件循环
asyncio.run(task)

async

  • 协程函数:定义函数时加上async修饰,即async def func()- 协程对象:执行协程函数得到的对象

注:执行协程函数得到协程对象,函数内部代码不会执行

# python 源码
>>> import asyncio

>>> async def main():
...     print('hello')
...     await asyncio.sleep(1)
...     print('world')

>>> main()
<coroutine object main at 0x1053bb7c8>

>>> asyncio.run(main())
hello
world

  • 执行协程函数内部代码,必须把协程对象交给事件循环处理

await

  • await + 可等待对象(协程对象,Future,Task对象(IO等待))- 等待到对象的返回结果,才会继续执行后续代码
# python 源码
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(1, 'hello')执行完之后,才继续向下执行
    await say_after(2, 'world')

    print(f"finished at {<!-- -->time.strftime('%X')}")

asyncio.run(main())

output:

started at 17:13:52 hello world finished at 17:13:55

asyncio.create_task()

  • asyncio.create_task()作为异步并发运行协程的函数Tasks。- 将协程添加到asyncio.create_task()中,则该协程将很快的自动计划运行
# python 源码
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')}")

output:

started at 17:14:32 hello world finished at 17:14:34

注:比不使用asyncio.create_task()的结果快了一秒,也即两个任务同时执行了。

asyncio.futures对象

  • 官方文档对Future的介绍如下

Futures A Future is a special low-level awaitable object that represents an eventual result of an asynchronous operation. When a Future object is awaited it means that the coroutine will wait until the Future is resolved in some other place. Future objects in asyncio are needed to allow callback-based code to be used with async/await. Normally there is no need to create Future objects at the application level code. Future objects, sometimes exposed by libraries and some asyncio APIs, can be awaited:

大概意思是: Future是特殊的低级等待对象,代表异步操作的最终结果。 当等待Future对象时,它意味着协程将等待,直到在其他地方解析Future。 需要在asyncio中使用将来的对象,以允许将基于回调的代码与async / await一起使用。 通常,不需要在应用程序级别的代码中创建Future对象。

  • 使用async/await时 会自动创建Future对象。(在此作为了解即可)

实例

  • 正常的网络请求
# python 源码
import requests
import time

def result(url):
    res = request_url(url)
    print(url,res)

def request_url(url):
    res = requests.get(url)
    print("\n",res)
    time.sleep(2)
    print("execute_time:",time.time()-start)
    return res

url_list = ["https://www.csdn.net/",
       "https://blog.csdn.net/qq_43380180/article/details/111573642",
       "https://www.baidu.com/",
       ]

start = time.time()
print("start_time:",start)
task = [result(url) for url in url_list]
endtime = time.time()-start
print("\nendtime:",time.time())
print("all_execute_time:",endtime)


output:

start_time: 1608888064.4879584 <Response [200]> execute_time: 4.062316656112671 https://www.csdn.net/ <Response [200]> <Response [200]> execute_time: 6.691046237945557 https://blog.csdn.net/qq_43380180/article/details/111573642 <Response [200]> <Response [200]> execute_time: 9.305423259735107 https://www.baidu.com/ <Response [200]> endtime: 1608888073.7933817 all_execute_time: 9.305423259735107

3个网络请求耗时9.3秒

  • 使用协程
# python 源码
import asyncio
import requests
import time


async def result(url):
    res = await request_url(url)
    print(url, res)


async def request_url(url):
    res = requests.get(url)
    print(url)
    await asyncio.sleep(2)
    print("execute_time:", time.time() - start)
    return res


url_list = ["https://www.csdn.net/",
            "https://blog.csdn.net/qq_43380180/article/details/111573642",
            "https://www.baidu.com/",
            ]

start = time.time()
print(f"start_time:{<!-- -->start}\n")

task = [result(url) for url in url_list]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(task))

endtime = time.time() - start
print("\nendtime:", time.time())
print("all_execute_time:", endtime)

output:

start_time:1608887916.4435036 https://blog.csdn.net/qq_43380180/article/details/111573642 https://www.baidu.com/ https://www.csdn.net/ execute_time: 2.6272454261779785 https://blog.csdn.net/qq_43380180/article/details/111573642 <Response [200]> execute_time: 3.0642037391662598 https://www.baidu.com/ <Response [200]> execute_time: 3.9802708625793457 https://www.csdn.net/ <Response [200]> endtime: 1608887920.4237745 all_execute_time: 3.9802708625793457

使用协程耗时3.9秒

对比结果可以看出,协程大大提高了程序的执行效率,程序数量越多,越能体现出协程对程序的执行效率越高!!!!

注:使用协程时,需要其底层方法实现时就是协程,才会生效,否则协程不生效!

  • 此处使用的requests底层实现并不是异步,因此使用了time.sleep()asyncio.sleep()模拟放大网络IO时间。- 异步模块举例:aiohttp-requests、aiofiles、grequests等
    文章中有不足或错误的地方,欢迎留言指正!

  • 协程(Coroutine)的主要使用场景:

    1. 异步编程:协程可以在单线程中实现异步编程,避免了多线程的开销和复杂性。常见的异步编程框架如 asyncio 和 tornado 都是基于协程实现的。

    2. IO密集型任务:协程适用于IO密集型任务,如网络请求、文件读写等等。通过使用协程,可以在等待IO完成的时候,自动切换到其他任务,从而提高程序的并发能力和响应速度。

    3. 高并发处理:协程可以轻松地实现大规模高并发的数据处理任务,如爬虫、数据分析等等。协程的高效率和低开销,使得它成为处理这类任务的首选方案。

    4. 模拟事件循环:协程可以模拟事件循环,实现非阻塞式的调用方式,从而提高程序的运行效率和响应速度。

    总之,协程的使用场景十分广泛,特别是在需要处理大量IO操作和高并发任务的情况下,协程的优势更加明显。

  • 协程读写文件时文件锁的使用

    • 在协程异步读写文件时,为了避免多个协程同时对同一个文件进行写操作而引起的数据不一致的问题,可以使用文件锁来实现文件的互斥访问。

      具体实现方式如下:

      1. 使用Python标准库中的fcntl模块,调用fcntl.flock函数对需要访问的文件进行加锁和解锁操作。

      2. 在协程异步读写文件时,首先获取文件锁,如果无法获取到文件锁则等待一段时间后重试,直到成功获取到文件锁。

      3. 协程完成文件读写操作后,释放文件锁,以便其他协程能够访问该文件。

      需要注意的是,在使用文件锁时需要注意锁的粒度,即针对整个文件还是只针对文件中的部分数据进行加锁。一般情况下,应该尽量减小锁的粒度,以便提高并发性能。

  • 下面是一个使用Python标准库中fcntl模块实现文件加锁的示例代码:

    import fcntl
    
    # 以写模式打开文件
    with open('file.txt', 'w') as f:
        # 获取文件锁(排他锁)
        fcntl.flock(f, fcntl.LOCK_EX)
        # 在锁定期间对文件进行操作
        f.write('Hello, world!')
        # 释放文件锁
        fcntl.flock(f, fcntl.LOCK_UN)
    

    在此示例中,通过调用fcntl.flock函数获取和释放文件锁。第二个参数指定所需的锁类型,fcntl.LOCK_EX表示排他锁(独占锁),该锁会阻止其他进程访问同一文件。如果需要共享锁,则可以将第二个参数设置为fcntl.LOCK_SH(共享锁)。

    请注意,在异步读写文件时,由于协程是单线程的,因此不必担心并发问题。但是,如果有多个进程或线程同时访问同一文件,则需要使用文件锁保护文件的完整性。

参考

  • 协程和任务: - 深入理解python异步 - Python异步编程 asyncio小白速通
  • https://blog.csdn.net/qq_43380180/article/details/111573642
  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值