python asyncio,即Python异步编程。

B站上的视频教程

asyncio?

asyncio,即异步,是指同时进行。

举个例子,烧10锅水,每锅水烧开的时间不一样,如果异步的话,那就是你一个一个点燃火,10口锅同时开始烧,最后耗时是用的时间最长的那个。相反,如果是同步,也就是第一口锅烧完了烧第二口过,以此类推,那么最后耗时就是所有耗时之和。

因此,异步存在的意义就是充分利用等待的时间,减少总时间消耗,从而加速

对于CPU来说,读入输出操作(IO)是最耗时间且需要等待的,因此一般使用异步来优化IO操作从而优化程序速度。

python asyncio

python中封装了asyncio库,用于异步编程,下载方法和使用方法都很python:

pip install asyncio
import asyncio

协程

asyncio基于协程操作。

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

这里将协程和多线程做个比较:还是拿上面的烧水举例,多线程是10口锅有10个人守着同时点火,协程是一个人看10口锅一个一个点火(一个点燃就点下一个)

(而一般的顺序流程是一个人看10口锅,每次要把一口锅点燃且等到水烧开了才去点下一口锅)

协程函数

一般来讲,如果把上面的烧水过程抽象为一个函数,一般就得这样写:

def boilWater(potID):
    BoilWater()  # 烧水,假设时间不定
    return True
for i in range(10):
    boilWater(i+1)

那么就得先boilWater(1),再boilWater(2),等等。

有没有一种方式能够让程序在第一口锅的火点燃之后就去点第二口的火而不用等待第一口锅烧开呢?有的:

async def boilWater(potID):
    # 使用async在def前面定义就可以让程序知道这个函数内部有需要等待的事情
    await BoilWater()  
    # 烧水。await的意义在于,程序运行到这里后知道这个函数需要等待,因此它可以去干点别的事去
    return True

需要知道的是,像这样定义的协程函数,直接调用它是无法执行其内部代码的。执行它只会得到一个协程对象。

而当程序遇到await,它怎么知道该去哪里呢?如何执行协程函数内部的代码?下面的事件循环来解决这两件事情。

事件循环

按照上面举的例子,我们可以发现,异步本质就是把一个任务分成几个互不干扰的任务同时进行(比如统计a班的人数和统计b班的人数是不干扰的)。

那么照这个逻辑,应该就有一个“东西”来统领这些互不干扰的子任务。详细的说,这个“东西”的功能主要应该有:

  • 能记录所有的子任务
  • 能不断的检查每个子任务的状态
  • 如果一个子任务完成了,那么解除对其的监视

asyncio库中封装好了这个“东西”,一般叫做事件循环,使用方法如下:

import asyncio
loop = asyncio.get_event_loop()  # 实例化一个事件循环

现在可以回答上面的协程函数中留的问题。

  • 当程序遇到await时,它就会去寻找事件循环中其他还可以做的不需要等待的事情,然后去做。等到之前await处需要等待的工作完成了,程序就会回到刚刚离开的await处继续工作。
  • 当协程函数放入事件循环中,它就会自动运行了

那么如何把单个协程函数放入事件循环中呢?python中提供了两种方法,按需选:

async def boilWater(potID):
    # 使用async在def前面定义就可以让程序知道这个函数内部有需要等待的事情
    await BoilWater()  
    # 烧水。await的意义在于,程序运行到这里后知道这个函数需要等待,因此它可以去干点别的事去
    return True

asyncio.run(boilWater(1))
# 或
loop = asyncio.get_event_loop()
loop.run_until_complete(boilWater(1))

# 第二种方法就是先实例化一个事件循环,然后将任务放进去开始跑
# 第一种方法是3.7后的python才有的,等价于第二种方法

如果想要取得当前正在运行的事件循环,使用:
asyncio.get_running_loop()

task对象

如果想要安排多个协程函数应该怎么办呢?这个时候就需要将协程函数变成一个task对象,然后批量放入事件循环中即可:

asyncio.create_task(boilWater(i)):可以将boilWater(i)这个协程函数变成一个task对象,同时把这个对象放入事件循环中

done, pending = await asyncio.wait(tasks, timeout=None):批量等待多个task对象(相当于给task中的每一个task对象都写了一个await),其中done, pending分别保存执行完后的task的返回值和超时执行的task,timeout参数规定超时时间。

因此,如果要写出上面烧水的代码,就可以这样:

import random
import asyncio

async def boilWater(potID):
    # 使用async在def前面定义就可以让程序知道这个函数内部有需要等待的事情
    timeWait = random.randint(0, 6)
    print("烧第{}锅,需要{}秒".format(potID, timeWait))
    await asyncio.sleep(timeWait)
    print("第{}锅烧完".format(potID))  
    # 烧水。await的意义在于,程序运行到这里后知道这个函数需要等待,因此它可以去干点别的事去
    return True

async def arrangement(): # 一般用一个函数来创建所有的协程函数任务
    tasks = [asyncio.create_task(boilWater(i)) for i in range(10)]
    await asyncio.wait(tasks)

asyncio.run(arrangement())
"""
如果想尝试一下第二种创建事件循环的方法,可以这样写:
loop = asyncio.get_event_loop()
loop.run_until_complete(arrangement())
效果一样的
"""

这个时候的输出就是:

烧第0锅,需要3秒
烧第1锅,需要1秒
烧第2锅,需要4秒
烧第3锅,需要4秒
烧第4锅,需要4秒
烧第5锅,需要6秒
烧第6锅,需要6秒
烧第7锅,需要3秒
烧第8锅,需要6秒
烧第9锅,需要0秒
第9锅烧完
第1锅烧完
第0锅烧完
第7锅烧完
第2锅烧完
第3锅烧完
第4锅烧完
第5锅烧完
第6锅烧完
第8锅烧完

await

如上所述,await表示这里可以等待,但是也不是所有的事情都可以等待,有些事情是程序需要去做的,因此,await关键字后的东西是有限制的。

协程对象,task对象,已经马上会说道的future对象,都可以放在await后面。

而像上面的程序中,我们将所有的任务放在一个列表里面,列表可不是可以放在await后面的对象。因此对于这种情况,使用asyncio.wait(tasks)就可以生成对应的多个task对象了

future对象

future对象就是一个一直等待别人给它传递结果的对象,如果没有别人给它传递结果,那么它就会一直等待。task对象继承了future对象。

import asyncio

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

async def main():
    loop = asyncio.get_running_loop()
    fut = loop.create_future()
    loop.create_task(set_after(fut))
    data = await fut
    print(data)
    
asyncio.run(main())

现在分析一下上面这个程序的运行逻辑就可以明白future对象是干啥的了:

首先创建事件循环,进入main函数

然后main函数的第一行将现在正在运行的循环拿出来,第二行就是创建了一个future对象(应该和loop实例没有关系,只是借助它创建的)。这个future对象说白了就只会干等,其他啥也不会,等着别人给它返回值。

然后运行到main函数的第三行,在事件循环中创建了一个任务,且将任务放到了事件循环中。

然后第四行,程序遇到了await,main函数就开始挂起,转到了事件循环中找可以执行的函数。而事件循环中有set_after函数,因此开始执行set——after函数,即睡眠两秒后,给fut(我们之前创建的future对象)设置了值"FINISH",然后函数执行完后,fut被赋了值,await的条件不再成立,函数继续运行

然后第五行,输出FINISH,然后结束运行