python协程系列(五)——asyncio的核心概念与基本架构

  参考:https://blog.csdn.net/qq_27825451/article/details/86218230

  声明:本文针对的是python3.4以后的版本的,因为从3.4开始才引入asyncio,后面的3.5 3.6 3.7版本是向前兼容的,只不过语法上面有稍微的改变。比如在3.4版本中使用@asyncio.coroutine装饰器和yield from语句,但是在3.5以后的版本中使用async、await两个关键字代替,虽然语法上稍微有所差异,但是原理是一样的。本文用最通俗的语言解释了pythonasyncio背后的一些核心概念,简要解析了asyncio的设计架构,并给出了使用python进行asyncio异步编程的一般模板。

  一,一些重要的概念

  1,协程(coroutine)-----本质就是一个函数

  所谓"协程"就是一个函数,这个函数需要有两个基本的组成要是,第一,需要使用@asyncio.coroutine进行装饰;第二,函数体内一定要有yield from返回的generator,或者使用yield from返回的一个另一个协程对象。

  当然,这两个条件并不是硬性规定的,如果没有这两个条件,依然是函数,只不过是普通函数而已。

  怎么判断一个函数是不是协程?通过asyncio.iscoroutine(obj)和asyncio.iscoroutinefunction(func)加以判断,返回true,则是。

  个人理解:协程函数需要使用@asyncio.coroutine装饰或者是使用关键字async定义,但是函数体内部不一定有yield from返回

  示例如下

import time
import asyncio

# 协程函数需要使用@asyncio.coroutine进行装饰,但是不一定函数体类有yield from返回
@asyncio.coroutine
def hello():
    pass

# 因为函数使用@asyncio.coroutine所以所以使用函数创建的对象h是一个协程对象
# 函数hello是一个协程函数,以下两个表达式都返回True
h = hello()
print(asyncio.iscoroutine(h))
# True
print(asyncio.iscoroutinefunction(hello))
# True

  那协程函数有什么作用

  (1) result = yield from future

  作用一:返回future的结果。什么是future?后面会讲到,当协程函数执行到这一句,协程会被悬挂起来,直到future的结果被返回。如果是future被中途取消,则会触发CancelledError异常。由于task是future的子类,后面也会解释,关于future的所有应用,都同样适用于task

  注意:暂无举例,因为我还不知道怎么创建一个future对象

  (2)result = yield from coroutine

  等待另一个协程函数返回结果或者触发异常

# 协程函数等待另一个coroutine返回或者触发异常
async def hello():
    print('begin')
    # asyncio.sleep(1)是一个coroutine
    # 但任务运行到coroutine时线程不会等待asyncio.sleep()运行
    # 而是直接中断并执行下一个消息循环,本次因为只有一个任务,所以没有看到线程执行其他的
    result = await asyncio.sleep(1)
    #result = time.sleep(6)
    print('hello返回的结果是{0}'.format(result))
    print('end')
loop = asyncio.get_event_loop()
loop.run_until_complete(hello())

  输出如下

begin
hello返回的结果是None
end

  asyncio.sleep(1)是一个协程这里用来模拟一个耗时的IO操作,没有返回所以返回None

  这里需要理解的是asyncio.sleep(1)于time.sleep(1)的不同,任务执行到asyncio.sleep(1)则立即中断,去执行其他任务,如果修改成time.sleep(1)则任务执行到这里是线程等待,不会去执行其他任务。

  示例说明

  定义一个tasks执行两次协程函数hello()

# 理解asyncio.sleep()和time.sleep()的不同
async def hello():
    print('begin')
    # asyncio.sleep(1)是一个coroutine
    # 但任务运行到coroutine时线程不会等待asyncio.sleep()运行
    result = await asyncio.sleep(1)
    #result = time.sleep(6)
    print('hello返回的结果是{0}'.format(result))
    print('end')

loop = asyncio.get_event_loop()
tasks = [hello(),hello()]
loop.run_until_complete(asyncio.wait(tasks))

  输出如下

begin
begin
hello返回的结果是None
end
hello返回的结果是None
end

  从输出结果可以看到两个begin是同时输出的而不是先完一次begin...end再输出一次

  解析:

  本次任务执行两次hello()当执行第一个hello()遇到await asyncio.sleep(1)时立即中断取执行第二个hello()所以开始打印了两个begin

  两个hello()几乎是同时执行的

  使用调试模式查看

  省略前面步骤直接到执行任务

 

 

 

 

 

 

 

 

 

 

 

   为了查看asyncio.sleep()和time.sleep()的区别,下面修改代码

# 把asyncio.sleep()改成time.sleep()则是线程sleep而不是协程的并行了
# 理解asyncio.sleep()和time.sleep()的不同
async def hello():
    print('begin')
    # asyncio.sleep(1)是一个coroutine
    # result = await asyncio.sleep(1)
    result = time.sleep(1)
    print('hello返回的结果是{0}'.format(result))
    print('end')


loop = asyncio.get_event_loop()
tasks = [hello(),hello()]
loop.run_until_complete(asyncio.wait(tasks))

  输出如下

begin
hello返回的结果是None
end
begin
hello返回的结果是None
end

  解析:因为在函数hello()内部没有使用asyncio.sleep()而是使用time.sleep()则任务运行到time.sleep()是当前线程暂停一段时间,线程不会中断而是继续等待一段时间再继续执行。所以两个任务并没有并发执行而是顺序执行。

  使用调试模式查看执行步骤

  省略前面若干步

 

 

 

 

 

 

 

 

 

   以下步骤为重复步骤,省略

  可以看到假如函数内部没有定义yield from或者是使用关键字await则函数还是一个普通函数,因为没有遇到协程则不会中断。

  如果想更加清晰看到协程的并发执行过程可以定义多个协程函数,内部使用asyncio.sleep()设置不同

async def hello():
    print('begin')
    # asyncio.sleep(1)是一个coroutine
    # 但任务运行到coroutine时线程不会等待asyncio.sleep()运行
    # 而是直接中断并执行下一个消息循环,本次因为只有一个任务,所以没有看到线程执行其他的
    result = await asyncio.sleep(4)
    # result = time.sleep(1)
    print('hello返回的结果是{0}'.format(result))
    print('end')

async def hello2():
    print('begin2')
    result = await asyncio.sleep(6)
    #result = time.sleep(4)
    print('hello2返回的结果是{0}'.format(result))
    print('end2')

loop = asyncio.get_event_loop()
tasks = [hello(),hello2()]
loop.run_until_complete(asyncio.wait(tasks))

  输出如下

begin2
begin
hello返回的结果是None
end
hello2返回的结果是None
end2

  为什么是先执行hello2()我也不清楚

  解析:执行hello2()输出完begin2后的await asyncio.sleep(6)同时开始执行hello所以先输出begin2 然后输出begin

    然后hello()和hello2()在同时执行因为hello()的sleep时间更短所以先执行完hello()执行完hello()过了2秒hello2()也执行完毕

  (3)result= yield from task

  返回一个task结果

  task 任务:一个协程对象就是一个原生可以挂起的函数,任务则是对协程进一步封装,其中包含任务的各种状态。

  示例如下

# yield from task
async def hello():
    print('begin')
    # 创建一个task,以下两种方法都可以创建task
    # task = asyncio.create_task(asyncio.sleep(5))
    task = asyncio.ensure_future(asyncio.sleep(1))
    result = await task
    print('hello返回的结果是{0}'.format(result))
    print('end')
    return 1

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

  输出如下

begin
hello返回的结果是None
end

  分析输出结果前了解怎么创建一个task

# asyncio.ensure_future创建一个task传递参数为一个coroutine对象
task = asyncio.ensure_future(asyncio.sleep(10))
# 类型为_asyncio.Task
print(type(task))
# <class '_asyncio.Task'>
# 运行
loop.run_until_complete(task)

  使用调试模式分析执行过程

  直接从执行协程函数开始分析

 

 

 

 

 

 

 

 

 

   以上执行的task是asyncio.sleep(1)返回为None,如果定义hello()协程函数一个返回为1 然后把hello()放入到task中,使用其他协程函数调用则可以获取一个return返回结果

async def hello2():
    task = asyncio.ensure_future(hello())
    result = await task
    print('hello2获取的返回{0}'.format(result))
loop.run_until_complete(hello2())

  输出如下

hello2获取的返回1

  因为task封装了协程hello()而在hello()定义了return 1所以执行协程hello2() await task的时候返回了hello()定义的返回值

  (4)return expression

  作为一个函数本身也是可以返回一个结果的

  示例如下

# return expression
async def hello():
    return '协程的返回结果'

loop = asyncio.get_event_loop()
result = loop.run_until_complete(hello())
print(result)
# 协程的返回结果

  个人理解:本次执行协程返回一个结果,和一个普通函数没什么区别。

  (5)raise exception 

  抛出错误

  示例如下

# raise exception 
async def hello():
    raise StopIteration()

loop = asyncio.get_event_loop()
result = loop.run_until_complete(hello())

  抛出错误中断执行

RuntimeError: coroutine raised StopIteration

  2,事件循环

  event_loop

  协程函数,不是想普通函数那样直接调用运行,必须添加到事件循环中,然后由事件循环去运行,单独运行协程函数是不会有结果的(单独运行协程函数返回一个coroutine对象)看一个例子

# 事件event_loop
async def say_after_time(delay,what):
    await asyncio.sleep(delay)
    print(what)
 
async def main():
    print(f"开始时间为: {time.time()}")
    await say_after_time(1,"hello")
    await say_after_time(2,"world")
    print(f"结束时间为: {time.time()}")
# 创建事件循环对象
loop=asyncio.get_event_loop()    
#与上面等价,创建新的事件循环
# loop=asyncio.new_event_loop()   
#通过事件循环对象运行协程函数
loop.run_until_complete(main()) 
# tasks = [main(),main()]
# loop.run_until_complete(asyncio.wait(tasks))
# 关闭
loop.close()

  输出如下

开始时间为: 1634363895.1841946
hello
world
结束时间为: 1634363898.1979322

  解析:

  本次定义了两个协程函数,协程函数say_after_time(delay,what)传递两个参数delay,what分别为整数和字符串,在协程函数内部又调用协程asyncio.sleep(delay)模拟协程操作,然后打印出传递的字符串what。函数main()也是一个协程函数,在函数内部分别调用两次协程函数say_after_time(),分别传递不同的参数。创建循环对象运行了协程函数main(),首先打印开始时间,然后调用协程函数say_after_time(delay,what),这个时候线程中断,等待协程函数say_after_time(delay,what)执行,如果本次任务执行了多个其他协程任务则不会等待去执行其他协程,但是本次任务队列只执行了main(),所以接着去执行第一个say_after_time(delay,what)了,在执行第一个say_after_time(delay,what)的时候遇到协程asyncio.sleep(delay)了,如果有其他协程也不会等待返回而去执行其他协程,本次没有,所以等这个asyncio.sleep(delay)返回结果以后又执行打印what了,然后第一个say_after_time(delay,what)执行完毕,接着执行第二个say_after_time(delay,what),过程类似不重复了。

   使用调试模式分析执行过程,从执行协程函数main()开始

 

 

 

 

 

 

 

 

 

 

 

   重复过程不列出

 

 

 

   以上代码看起来是顺序运行,是因为任务只定义了一个协程所以好像还是顺序运行并没有实现协程的并发运行,修改代码定义一个task运行两个协程

# 事件event_loop
async def say_after_time(delay,what):
    await asyncio.sleep(delay)
    print(what)
 
async def main():
    print(f"main开始时间为: {time.time()}")
    await say_after_time(1,"hello1")
    await say_after_time(2,"world1")
    print(f"main结束时间为: {time.time()}")

async def main2():
    print(f"main2开始时间为: {time.time()}")
    await say_after_time(1,"hello2")
    await say_after_time(2,"world2")
    print(f"main2结束时间为: {time.time()}")
# 创建事件循环对象
loop=asyncio.get_event_loop()    

#通过事件循环对象运行协程函数
# loop.run_until_complete(main()) 
tasks = [main(),main2()]
loop.run_until_complete(asyncio.wait(tasks))
# 关闭
loop.close()

  为了区分本次定义了两个协程函数

  运行输出如下

main2开始时间为: 1634371608.927798
main开始时间为: 1634371608.927798
hello2
hello1
world2
main2结束时间为: 1634371611.9465902
world1
main结束时间为: 1634371611.9465902

  通过输出可以看出main2和main1几乎是同时运行,同时结束的,如果不是协程而是普通函数输出应该是向下面这样有顺序的分别输出,不可能交叉输出

main2开始时间为: 1634371608.927798
hello2
world2
main2结束时间为: 1634371611.9465902

main开始时间为: 1634371608.927798
hello1
world1
main结束时间为: 1634371611.9465902

  如果我们单独像执行普通函数那样执行一个协程函数,只会返回一个coroutine对象(python3.7)如下所示:

<coroutine object hello at 0x000001B3B5345D40>

  如果直接运行会返回coroutine对象并且报一个警告,注意是警告不是错误

RuntimeWarning: Enable tracemalloc to get the object allocation traceback

  (1)获取事件循环对象的几种方法

  下面几种方法可以用来获取,设置,创建事件循环对象loop

  loop=asyncio.get_running_loop() 返回(获取)在当前线程中正在运行的事件循环,如果没有正在运行的事件循环,则会显示错误;它是python3.7中新添加的

  使用该方法获取事件循环需要在事件运行期间才能获取,不能再事件运行之后获取,因为事件运行之后相当于该事件已经运行完毕就无法获取了

  举例说明

# 获取事件循环对象的几种方式
# asyncio.get_running_loop()获取当前的事件循环对象
async def hello():
    result = await asyncio.sleep(1)
    # 获取当前事件循环对象并打印
    loop = asyncio.get_running_loop()
    print(loop)

# 创建事件循环对象
loop = asyncio.get_event_loop()
# 运行事件循环对象
loop.run_until_complete(hello())

  输出如下

<ProactorEventLoop running=True closed=False debug=False>

  其中各个选项含义如下

# 正在运行
running=True
# 还没有关闭
closed=False
# 没有开启debug 
debug=False

  如果是在事件循环外打印事件循环对象

# 获取事件循环对象的几种方式
# asyncio.get_running_loop()获取当前的事件循环对象
async def hello():
    result = await asyncio.sleep(1)
    # 获取当前事件循环对象并打印
    loop = asyncio.get_running_loop()
    print(loop)

# 创建事件循环对象
loop = asyncio.get_event_loop()
# 运行事件循环对象
loop.run_until_complete(hello())
# <ProactorEventLoop running=True closed=False debug=False>
print(loop)

  则输出如下,事件循环对象已经创建但是running的状态为False即事件循环对象没有运行或者是已经运行完毕

<ProactorEventLoop running=False closed=False debug=False>

  如果在事件循环对象运行之外使用get_running_loop()获取事件循环对象则会报错

# 获取事件循环对象的几种方式
# asyncio.get_running_loop()获取当前的事件循环对象
async def hello():
    result = await asyncio.sleep(1)
    # 获取当前事件循环对象并打印
    loop = asyncio.get_running_loop()
    print(loop)

# 创建事件循环对象
loop = asyncio.get_event_loop()
asyncio.get_running_loop()

  报错信息如下

RuntimeError: no running event loop

  为什么会报错,因为事件循环对象没有运行或者是运行结束,当代码执行到asyncio.get_running_loop()这一步时,当前线程并没有正在运行事件循环对象。

  loop=asyncio.get_event_loop() 获得一个事件循环,如果当前线程还没有事件循环,则创建一个新的事件循环loop;

  不详述,前面代码都是使用这个方法创建一个新的事件循环loop

  loop=asyncio.set_event_loop(loop) 设置一个事件循环为当前线程的事件循环;

  个人注释:使用这个方法返回为None,不知道这个怎么使用

  loop=asyncio.new_event_loop() 创建一个新的事件循环
  和asyncio.get_event_loop() 效果一样

  (2)通过事件循环运行协程函数的两种方式

  (1)方式一:创建循环对象loop,即asyncio.get_event_loop(),然后通过loop.run_until_complete()方法来运行。

  (2)方式二:直接通过asyncio.run(function_name)运行协程函数。但是需要注意的是,首先run函数是python3.7版本新添加的,前面的版本是没有的;其次,这个run函数总是创建一个新的实践循环并在run结束之后关闭事件循环,所以,如果在同一个线程中已经有了一个事件循环,则不能再使用再给函数了,因为同一个线程不能有两个事件循环,而且这个run函数不能同时运行两次,因为它已经创建一个了。即同一个线程是不允许有多个事件循环loop的。

  举例说明

# 运行协程
async def hello():
    print('begin')
    result = await asyncio.sleep(1) 
    print('end')
   
loop = asyncio.get_event_loop()
loop.run_until_complete(hello())
loop.close()
asyncio.run(hello())

  输出如下

begin
end
begin
end

  上述例子中,以下代码实现的效果是一样的 

loop = asyncio.get_event_loop()
loop.run_until_complete(hello())
loop.close()

  使用run

asyncio.run(hello())

  个人理解:使用asyncio.run会自动关闭,类似于使用with命令打开文件。

  以下所说同一个线程不允许有多个事件循环,但是上例有两个事件循环,是因为虽然有两个事件循环但是其实他们是顺序执行的,执行第一个事件循环结束以后再执行第二个事件循环,所以不冲突,是允许的。

  以下例子示例一个线程只能运行一个事件循环

# 一个线程值同时只允许执行一个事件循环
async def hello():
    print('begin')
    await asyncio.sleep(1)
    asyncio.run(asyncio.sleep(1))
    print('end')

asyncio.run(hello())

  运行报错

 

 

   即不能在事件循环内再运行一个事件循环

   注意:到底什么是事件循环?如何理解?

  可以这样理解:线程一直在各个协程方法直接永不停歇的游走,遇到一个yield from或await就悬挂起来,然后又走到另外一个方法,依次进行下去,直到事件循环所有的方法执行完毕。实际上loop是BaseEventLoop的一个实例,我们可以查看定义,它到底有哪些方法可调用。

  3,什么是awaitable对象----即可暂停等待对象

  有三类对象是可等待的,即 coroutines, Tasks, and Futures.

 

  coroutine:本质上就是一个函数,一前面的生成器yield和yield from为基础,不再赘述;

 

  Tasks: 任务,顾名思义,就是要完成某件事情,其实就是对协程函数进一步的封装;

 

  Future:它是一个“更底层”的概念,他代表一个一步操作的最终结果,因为一步操作一般用于耗时操作,结果不会立即得到,会在“将来”得到异步运行的结果,故而命名为Future。

 

  三者的关系,coroutine可以自动封装成task,而Task是Future的子类。

  4,什么是task任务

  如前所述,Task用来并发调度的协程,即对协程函数的进一步包装?那为什么还需要包装呢?因为单纯的协程函数仅仅是一个函数而已,将其包装成任务,任务是可以包含各种状态的,异步编程最重要的就是对异常操作状态的把控了。

  (1)创建任务(两种方法)

  方法一:task = asyncio.create_task(coro())   # 这是3.7版本新添加的

  方法二:task = asyncio.ensure_future(coro())

  两种方法都可以创建task有什么区别?

  使用create_task创建task需要在一个循环事件内创建,即在运行循环事件的时候创建,而不能在循环事件之外创建,举例说明

# create_task创建task需要在一个循环事件内创建
async def hello():
    print('start')
    task = asyncio.create_task(asyncio.sleep(1))
    result = await task
    print('task返回结果为{0}'.format(result))

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

  输出如下

start
task返回结果为None

  分析,运行协程函数hello()首先打印start,然后使用create_task(asyncio.sleep(1))创建一个task,该task完成一个sleep的操作,这个时候只是创建task并没有运行,到下一步await的时候才执行然后等待1秒以后输出 task返回结果为None

  以上运行的task没有返回所以返回None下面例子给协程函数hello()定义一个返回,然后在协程函数main里面创建task调用hello()

# create_task创建task需要在一个循环事件内创建
async def hello():
    print('start')
    task = asyncio.create_task(asyncio.sleep(1))
    result = await task
    print('task返回结果为{0}'.format(result))
    # 自定义返回
    return 'hello的返回'

async def main():
    task = asyncio.create_task(hello())
    result = await task
    print(result)

loop = asyncio.get_event_loop()
# loop.run_until_complete(hello())
loop.run_until_complete(main())

  输出如下

start
task返回结果为None
hello的返回

  解析

 

 

 

 

 

   不能在一个循环事件之外使用该方法创建task,运行报错

task = asyncio.create_task(hello())

  运行报错如下

RuntimeError: no running event loop

  创建task的方法二示例,在循环事件之外创建task

# asyncio.ensure_future在循环事件外创建task
async def hello():
    print('start')
    task = asyncio.create_task(asyncio.sleep(1))
    result = await task
    print('task返回结果为{0}'.format(result))

task = asyncio.ensure_future(hello())
loop.run_until_complete(task)

  也可以使用

  loop.create_future()

  loop.create_task(coro)

  示例

# create_future创建task示例start
def hello(future):
    print('start')
    # result = await asyncio.sleep(1)
    future.set_result(1)
    print('end')

loop = asyncio.get_event_loop()
task = loop.create_future()
loop.call_soon(hello,task)
loop.run_until_complete(task)
# create_future创建task示例end

  

  loop.call_soon()传递参数第一个为函数名,第二个为创建的task ,定义的函数是普通函数

  注意:create_future具体怎么使用还不了解

  使用create_task创建task示例

# create_task创建task示例start
async def hello():
    print('start')
    result = await asyncio.sleep(1)
    print('end')

loop = asyncio.get_event_loop()
# 创建task传递参数为运行函数hello()即一个协程对象
task = loop.create_task(hello())
# 运行
loop.run_until_complete(task)
# create_task创建task示例end

  备注:关于任务的详解,会在后面的系列文章继续讲解,本文只是概括性的说明。

  (2)获取某一个任务的方法

  方法一:task=asyncio.current_task(loop=None)

  返回在某一个指定的loop中,当前正在运行的任务,如果没有任务正在运行,则返回None;

  如果loop为None,则默认为在当前的事件循环中获取,

  方法二:asyncio.all_tasks(loop=None)

  返回某一个loop中还没有结束的任务
  示例:

# asyncio.current_task获取一个任务的方法start
async def hello():
    print('start')
    result = await asyncio.sleep(1)
    task = asyncio.current_task()
    # task = asyncio.all_tasks()
    print(task)
    print('end')

loop = asyncio.get_event_loop()
task = asyncio.ensure_future(hello())
loop.run_until_complete(task)

# asyncio.current_task获取一个任务的方法end

  输出如下

PS D:\learn-python3\函数式编程> & C:/ProgramData/Anaconda3/python.exe d:/learn-python3/学习脚本/协程系列/use_asyncio.py   
start
<Task pending name='Task-1' coro=<hello() running at d:/learn-python3/学习脚本/协程系列/use_asyncio.py:256> cb=[_run_until_complete_cb() at C:\ProgramData\Anaconda3\lib\asyncio\base_events.py:184]>
end

  同理返回任务需要在一个事件循环内,方法asyncio.all_tasks()在本次得到到输出和asyncio.current_task()是一致的

  5,什么是future

  Future是一个较底层的可等待(awaitable)对象,它表示的是异步操作的最终结果,当一个Future对象被等待的时候,协程会一直等待,直到Future已经运算完毕。

  Future是Task的父类,一般情况下,已不用去管它们两者的详细区别,也没有必要去用Future,用Task就可以了。  

  返回 future 对象的低级函数的一个很好的例子是 loop.run_in_executor().

  二,asyncio的基本构架

  前面介绍了asyncio里面最为核心的几个概念,如果能够很好地理解这些概念,对于学习协程是非常有帮助的,但是按照我个人的风格,我会先说asyncio的架构,理解asyncio的设计架构有助于更好地应用和理解。

  asyncio分为高层API和低层API,我们都可以使用,就像我前面在讲matplotlib的架构的时候所讲的一样,我们前面所讲的Coroutine和Tasks属于高层API,而Event Loop 和Future属于低层API。当然asyncio所涉及到的功能远不止于此,我们只看这么多。下面是是高层API和低层API的概览:

  High-level APIs

  Coroutines and Tasks(本文要写的)
  Streams
  Synchronization Primitives
  Subprocesses
  Queues
  Exceptions
  Low-level APIs

  Event Loop(下一篇要写的)
  Futures
  Transports and Protocols
  Policies
  Platform Support

  所谓的高层API主要是指那些asyncio.xxx()的方法,

  1,常见的以下高层API方法

  (1)运行异步协程

  asyncio.run(coro*debug=False)  #运行一个一步程序,参见上面

  (2)创建任务

  task=asyncio.create_task(coro)  #python3.7  ,参见上面

  task = asyncio.ensure_future(coro()) 

  (3)睡眠

  await asyncio.sleep(delay, result=None, *, loop=None)

  这个函数表示的是:当前的那个任务(协程函数)睡眠多长时间,而允许其他任务执行。这是它与time.sleep()的区别,time.sleep()是当前线程休息,注意他们的区别哦。

  另外如果提供了参数result,当当前任务(协程)结束的时候,它会返回;

  loop参数将会在3.10中移除,这里就不再说了。
  (4)并发运行多个任务

  await asyncio.gather(*coros_or_futures, loop=Nonereturn_exceptions=False)

  它本身也是awaitable的。

  *coros_or_futures是一个序列拆分操作,如果是以个协程函数,则会自动转换成Task。

  当所有的任务都完成之后,返回的结果是一个列表的形式,列表中值的顺序和*coros_or_futures完成的顺序是一样的。

  return_exceptions:False,这是他的默认值,第一个出发异常的任务会立即返回,然后其他的任务继续执行;

  True,对于已经发生了异常的任务,也会像成功执行了任务那样,等到所有的任务执行结束一起将错误的结果返回到最终的结果列表里面。  

  如果gather()本身被取消了,那么绑定在它里面的任务也就取消了。

  (5)防止任务取消

  await asyncio.shield(*arg, *, loop=None)

  它本身也是awaitable的。顾名思义,shield为屏蔽、保护的意思,即保护一个awaitable 对象防止取消,一般情况下不推荐使用,而且在使用的过程中,最好使用try语句块更好。

try:
    res = await shield(something())
except CancelledError:
    res = None

  (6)设置timeout——一定要好好理解

  await asyncio.wait_for(aw, timeout, *, loop=None)

  如果aw是一个协程函数,会自动包装成一个任务task。参见下面的例子:

import asyncio
 
async def eternity():
    print('我马上开始执行')
    await asyncio.sleep(3600)  #当前任务休眠1小时,即3600秒
    print('终于轮到我了')
 
async def main():
    # Wait for at most 1 second
    
    try:
        print('等你3秒钟哦')
        await asyncio.wait_for(eternity(), timeout=3)  #休息3秒钟了执行任务
    except asyncio.TimeoutError:
        print('超时了!')
 
asyncio.run(main())

  

  运行结果如下

等你3秒钟哦
我马上开始执行
超时了!

  解析:首先调用main()入口函数,当输出"等你3秒哦",main()挂起,执行eternity(),然后打印“我马上开始执行”然后遇到await挂起,而且需要挂起3600秒,大于timeout设置的3秒,这时候触发TimeoutError输出“超时了!”

  修改代码把eternity的模拟等待时间始终为2秒

import asyncio
 
async def eternity():
    print('我马上开始执行')
    await asyncio.sleep(2)  #当前任务休眠1小时,即3600秒
    print('终于轮到我了')
 
async def main():
    # Wait for at most 1 second
    
    try:
        print('等你3秒钟哦')
        await asyncio.wait_for(eternity(), timeout=3)  #休息3秒钟了执行任务
    except asyncio.TimeoutError:
        print('超时了!')
 
asyncio.run(main())

  输出如下

等你3秒钟哦
我马上开始执行
终于轮到我了

  因为eternity等待的时间为2秒,小于设置的timeout时间3秒,所以没有错发TimeoutError所以eternity完整执行了。

  总结:当异步操作需要执行的时间超过waitfor设置的timeout,就会触发异常,所以在编写程序的时候,如果要给异步操作设置timeout,一定要选择合适,如果异步操作本身的耗时较长,而你设置的timeout太短,会涉及到她还没做完,就抛出异常了。

   (7)多个协程函数时候的等待

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

  与上面的区别是,第一个参数aws是一个集合,要写成集合set的形式,比如:

  {func(),func(),func3()}

  表示的是一系列的协程函数或者是任务,其中协程会自动包装成任务。事实上,写成列表的形式也是可以的。

  注意:该函数的返回值是两个Tasks/Futures的集合:

  (done, pending)

  其中done是一个集合,表示已经完成的任务tasks;pending也是一个集合,表示还没有完成的任务。

  常见的使用方法为:done, pending = await asyncio.wait(aws)

  参数解释:

  timeout (a float or int), 同上面的含义一样,需要注意的是,这个不会触发asyncio.TimeoutError异常,如果到了timeout还有任务没有执行完,那些没有执行完的tasks和futures会被返回到第二个集合pending里面。

  return_when参数,顾名思义,他表示的是,什么时候wait函数该返回值。只能够取下面的几个值。
  

Constant Description
FIRST_COMPLETED 当任何一个task或者是future完成或者是取消,wait函数就返回
FIRST_EXCEPTION  当任何一个task或者是future触发了某一个异常,就返回,.如果是所有的task和future都没有触发异常,则等价与下面的 ALL_COMPLETED
ALL_COMPLETED  当所有的task或者是future都完成或者是都取消的时候,再返回。

 
  如下面例子所示

import asyncio
import time
 
a=time.time()
 
async def hello1():  #大约2秒
    print("Hello world 01 begin")
    await asyncio.sleep(2)
    print("Hello again 01 end")
 
async def hello2():  #大约3秒
    print("Hello world 02 begin")
    await asyncio.sleep(3)
    print("Hello again 02 end")
 
async def hello3():  #大约4秒
    print("Hello world 03 begin")
    await asyncio.sleep(4)
    print("Hello again 03 end")
 
async def main():   #入口函数
    done,pending=await asyncio.wait({hello1(),hello2(),hello3()},return_when=asyncio.FIRST_COMPLETED)
    for i in done:
        print(i)
    for j in pending:
        print(j)
 
asyncio.run(main()) #运行入口函数
 
b=time.time()
print('---------------------------------------')
print(b-a)

  输出如下

Hello world 02 begin
Hello world 01 begin
Hello world 03 begin
Hello again 01 end
<Task finished name='Task-3' coro=<hello1() done, defined at d:/learn-python3/学习脚本/协程系列/use_asyncio.py:298> result=None>
<Task pending name='Task-2' coro=<hello2() running at d:/learn-python3/学习脚本/协程系列/use_asyncio.py:305> wait_for=<Future pending cb=[<TaskWakeupMethWrapper object at 0x000002BAE8B04AF0>()]>>
<Task pending name='Task-4' coro=<hello3() running at d:/learn-python3/学习脚本/协程系列/use_asyncio.py:310> wait_for=<Future pending cb=[<TaskWakeupMethWrapper object at 0x000002BAE8B04B50>()]>>
---------------------------------------
2.030433177947998

  从上面可以看出,hello1()运行结束了,hello2()和hello3()还没有结束

  因为参数设置为

return_when=asyncio.FIRST_COMPLETED

  所以当任何一个task或者是future完成或者是取消,wait函数就返回,因为hello1()等待的时间最短所以执行完就返回了,但是这个时候hello2()和hello3()还没有执行完毕,强迫中断了,所以done为已完成的task集合即hello1(),pending为未完成的task集合即hello2() hello3()

  (8)asyncio.as_completed()函数

  asyncio.as_completed(aws, *, loop=None, timeout=None)

  第一个参数aws:同上面一样,是一个集合{}集合里面的元素是coroutine、task或者future

  第三个参数timeout:意义和上面讲的的一样

  那到底什么作用呢?

# asyncio.as_completed start
import asyncio
import time
import threading
 
a=time.time()
 

async def hello1():
    print("Hello world 01 begin")
    await asyncio.sleep(5)  #大约5秒
    print("Hello again 01 end")
    return '哈哈1'
 

async def hello2():
    print("Hello world 02 begin")
    await asyncio.sleep(3) #大约3秒
    print("Hello again 02 end")
    return '哈哈2'
 

async def hello3():
    print("Hello world 03 begin")
    await asyncio.sleep(4) #大约4秒
    print("Hello again 03 end")
    return '哈哈3'
 
async def main():
    s=asyncio.as_completed({hello1(),hello2(),hello3()})
    for f in s:
        result=await f
        print(result)
    
asyncio.run(main())
 
b=time.time()
print('---------------------------------------')
print(b-a)
# asyncio.as_completed end

  输出如下

Hello world 03 begin
Hello world 01 begin
Hello world 02 begin
Hello again 02 end
哈哈2
Hello again 03 end
哈哈3
Hello again 01 end
哈哈1
---------------------------------------
5.02417516708374

  结论:asyncio.as_completed()函数返回的是一个可迭代(iterator)的对象,对象的每个元素就是一个future对象,很多小伙伴说,这不是相当于没变吗?其实返回的future集合是对参数的future集合重新组合,组合的顺序就是,最先执行完的协程函数(coroutine、task、future)最先返回,从上面的代码可知,参数为

  aws={hello1(),hello2(),hello3()},因为hello1大约花费5秒、hello2大约花费3秒、hello3大约花费4秒。返回的结果为

  s={hello2()、hello3()、hello(1)},因为hello2时间最短,故而放在前面,hello1时间最长,故而放在最后面。然后对返回的集合s开始迭代。

  2,task类详解

  官方英文文档如下

class asyncio.Task(coro, *, loop=None)

A Future-like object that runs a Python coroutine. Not thread-safe.

Tasks are used to run coroutines in event loops. If a coroutine awaits on a Future, the Task suspends the execution of the coroutine and waits for the completion of the Future. When the Future is done, the execution of the wrapped coroutine resumes.

Event loops use cooperative scheduling: an event loop runs one Task at a time. While a Task awaits for the completion of a Future, the event loop runs other Tasks, callbacks, or performs IO operations.

Use the high-level asyncio.create_task() function to create Tasks, or the low-level loop.create_task() or ensure_future() functions. Manual instantiation of Tasks is discouraged.

To cancel a running Task use the cancel() method. Calling it will cause the Task to throw a CancelledError exception into the wrapped coroutine. If a coroutine is awaiting on a Future object during cancellation, the Future object will be cancelled.

cancelled() can be used to check if the Task was cancelled. The method returns True if the wrapped coroutine did not suppress the CancelledError exception and was actually cancelled.

asyncio.Task inherits from Future all of its APIs except Future.set_result() and Future.set_exception().

Tasks support the contextvars module. When a Task is created it copies the current context and later runs its coroutine in the copied context.

  

  上面的文字描述中推出了几个非常重要的信息,特在此总结如下:

  (1)他是作为一个python协程对象,和Future对象很像的这么一个对象,但不是线程安全的;他继承了Future所有的API,,除了Future.set_result()和Future.set_Exception();

  (2)使用高层API asyncio.ccreate_task()创建任务,或者是使用低层API loop.create_task()或者是loop.ensure_future()创建任务对象;

  (3)相比于协程函数,任务时有状态的,可以使用Task.cancel()进行取消,这会触发CancelledError异常,使用cancelled()检查是否取消。

  下面介绍Task类常见的一些使用函数

  (1)cancel()

  Request the Task to be cancelled.

  其实前面已经有所介绍,最好是使用他会出发CancelledError异常,所以需要取消的协程函数里面的代码最好在try-except语句块中进行,这样方便触发异常,打印相关信息,但是Task.cancel()没有办法保证任务一定会取消,而Future.cancel()是可以保证任务一定取消的。可以参见下面的一个例子:

# task.cancel() start
import asyncio
 
async def cancel_me():
    print('cancel_me(): before sleep')
    try:
        await asyncio.sleep(3600) #模拟一个耗时任务
    except asyncio.CancelledError:
        print('cancel_me(): cancel sleep')
        raise
    finally:
        print('cancel_me(): after sleep')
 
async def main():
    #通过协程创建一个任务,需要注意的是,在创建任务的时候,就会跳入到异步开始执行
    #因为是3.7版本,创建一个任务就相当于是运行了异步函数cancel_me
    task = asyncio.create_task(cancel_me()) 
    #等待一秒钟
    await asyncio.sleep(1)
    print('main函数休息完了')
    #发出取消任务的请求
    task.cancel()  
    try:
        await task  #因为任务被取消,触发了异常
    except asyncio.CancelledError:
        print("main(): cancel_me is cancelled now")
 
asyncio.run(main())

# task.cancel() end

  输出如下

cancel_me(): before sleep
main函数休息完了
cancel_me(): cancel sleep
cancel_me(): after sleep
main(): cancel_me is cancelled now

  运行过程分析:

  首先run函数启动主函数入口main,在main,因为第一个话就是调用异步函数cancel_me(),函数,所以先打印出第一句话;

  然后进入cancle_me中的tty语句,遇到await,暂停,这时返回main中执行,但是又在main中遇到await,也会暂停,但是由于main中只需要暂停1秒,而camcel_me中要暂停3600秒,所以等到main暂停结束后,接着运行main,所以打印出第二句话;

  接下来遇到取消任务的请求task.cancel(),然后继续执行main里面的try,又遇到await,接着main进入暂停,接下来进入到cancle_me函数中,但是由于main中请求取消任务,所以那个耗时3600秒的任务就不再执行了,直接触发了CancelldeError异常,打印出第三句话,接下来raise一个异常信息

  接下来执行cancel_me的finally,打印出第四句话,此时cancel_me执行完毕,由于它抛出了一个异常,返回到主程序main中,触发异常,打印出第五句话。

  (2)done()

  当一个被包装得协程既没有触发异常、也没有被取消的时候,意味着它是done的,返回true。

  (3)result()

  返回任务的执行结果,

  当任务被正常执行完毕,则返回结果;

  当任务被取消了,调用这个方法,会触发CancelledError异常;

  当任务返回的结果是无用的时候,则调用这个方法会触发InvalidStateError;

  当任务出发了一个异常而中断,调用这个方法还会再次触发这个使程序中断的异常。

  (4)exception()

  返回任务的异常信息,触发了什么异常,就返回什么异常,如果任务是正常执行的无异常,则返回None;

  当任务被取消了,调用这个方法会触发CancelledError异常;

  当任务没有做完,调用这个方法会触发InvalidStateError异常。

  下面还有一些不常用的方法,如下:

  (5)add_done_callback(callback, *, context=None)

  (6)remove_done_callback(callback)

  (7)get_stack(*, limit=None)

  (8)print_stack(*, limit=None, file=None)

  (9)all_tasks(loop=None),这是一个类方法

  (10)current_task(loop=None),这是一个类方法

  3,异步函数结果的获取

  对于异步编程,异步函数而言,最重要的就是异步函数调用结束之后,获取异步函数的返回值,我们可以用以下几种方式来获取函数的返回值,第一个直接通过Task.reslut()来获取;第二种是绑定一个回调函数来获取,即函数执行完毕后调用一个函数来获取异步函数的返回值。

  (1)直接通过result获取

# 通过result获取 start
async def hello1(a,b):
    print("Hello world 01 begin")
    await asyncio.sleep(3)  #模拟耗时任务3秒
    print("Hello again 01 end")
    return a+b
 
coroutine=hello1(10,5)
loop = asyncio.get_event_loop()                #第一步:创建事件循环
task=asyncio.ensure_future(coroutine)         #第二步:将多个协程函数包装成任务列表
loop.run_until_complete(task)                  #第三步:通过事件循环运行
print('-------------------------------------')
print(task.result())
loop.close() 

# 通过result获取 end

  输出如下

Hello world 01 begin
Hello again 01 end
-------------------------------------
15

  (2)通过定义回调函数来获取

# 通过回调函数获取 start
import asyncio
import time
 
 
async def hello1(a,b):
    print("Hello world 01 begin")
    await asyncio.sleep(3)  #模拟耗时任务3秒
    print("Hello again 01 end")
    return a+b
 
def callback(future):   #定义的回调函数
    print(future.result())
 
loop = asyncio.get_event_loop()                #第一步:创建事件循环
task=asyncio.ensure_future(hello1(10,5))       #第二步:将多个协程函数包装成任务
task.add_done_callback(callback)               #给任务绑定一个回调函数
 
loop.run_until_complete(task)                  #第三步:通过事件循环运行
loop.close()    
# 通过回调函数获取 end

  注意:所谓的回调函数,就是指协程函数coroutine执行结束时候会调用回调函数。并通过参数future获取协程执行的结果。我们创建的task和回调里的future对象,实际上是同一个对象,因为task是future的子类。

  三,asyncio异步编程的基本模板

  事实上,在使用asyncio进行异步编程的时候,语法形式往往是多样性的,虽然理解异步编程的核心思想很重要,但是实现的时候终究还是要编写语句的,本次给出的模板,是两个不同的例子,例子一是三个异步方法,它们都没有参数,没有返回值,都模拟一个耗时任务;例子二是三个异步方法,都有参数,都有返回值。

  1,Python3.7之前版本

  (1)例子一:无参数,无返回值

# 3.7版本之前 无参数无返回值 start

import asyncio
import time
 
a=time.time()
 
async def hello1():
    print("Hello world 01 begin")
    await asyncio.sleep(3)  #模拟耗时任务3秒
    print("Hello again 01 end")
 
async def hello2():
    print("Hello world 02 begin")
    await asyncio.sleep(2)   #模拟耗时任务2秒
    print("Hello again 02 end")
 
async def hello3():
    print("Hello world 03 begin")
    await asyncio.sleep(4)   #模拟耗时任务4秒
    print("Hello again 03 end")
 
loop = asyncio.get_event_loop()                #第一步:创建事件循环
tasks = [hello1(), hello2(),hello3()]          #第二步:将多个协程函数包装成任务列表
loop.run_until_complete(asyncio.wait(tasks))   #第三步:通过事件循环运行
loop.close()                                   #第四步:取消事件循环


# 3.7版本之前 无参数无返回值 end

  (2)例子2 有参数有返回值

# 3.7版本之前 有参数有返回值 start

import asyncio
import time
 
 
async def hello1(a,b):
    print("Hello world 01 begin")
    await asyncio.sleep(3)  #模拟耗时任务3秒
    print("Hello again 01 end")
    return a+b
 
async def hello2(a,b):
    print("Hello world 02 begin")
    await asyncio.sleep(2)   #模拟耗时任务2秒
    print("Hello again 02 end")
    return a-b
 
async def hello3(a,b):
    print("Hello world 03 begin")
    await asyncio.sleep(4)   #模拟耗时任务4秒
    print("Hello again 03 end")
    return a*b
 
loop = asyncio.get_event_loop()                #第一步:创建事件循环
task1=asyncio.ensure_future(hello1(10,5))
task2=asyncio.ensure_future(hello2(10,5))
task3=asyncio.ensure_future(hello3(10,5))
tasks = [task1,task2,task3]                    #第二步:将多个协程函数包装成任务列表
loop.run_until_complete(asyncio.wait(tasks))   #第三步:通过事件循环运行
print(task1.result())                               #并且在所有的任务完成之后,获取异步函数的返回值   
print(task2.result())
print(task3.result())
loop.close()                                   #第四步:关闭事件循环


# 3.7版本之前 有参数有返回值 end

  输出如下

Hello world 01 begin
Hello world 02 begin
Hello world 03 begin
Hello again 02 end
Hello again 01 end
Hello again 03 end
15
5
50

  (3)总结:四步走(针对3.7之前版本)

  第一步:构建事假循环

loop=asyncio.get_running_loop() #返回(获取)在当前线程中正在运行的事件循环,如果没有正在运行的事件循环,则会显示错误;它是python3.7中新添加的
 
loop=asyncio.get_event_loop() #获得一个事件循环,如果当前线程还没有事件循环,则创建一个新的事件循环loop;
 
loop=asyncio.set_event_loop(loop) #设置一个事件循环为当前线程的事件循环;
 
loop=asyncio.new_event_loop()  #创建一个新的事件循环

  第二步:将一个或是多个协程函数包装成任务Task

#高层API
task = asyncio.create_task(coro(参数列表))   # 这是3.7版本新添加的
task = asyncio.ensure_future(coro(参数列表)) 
 
#低层API
loop.create_future(coro)
loop.create_task(coro)

  第三步:通过实践循环运行

loop.run_until_complete(asyncio.wait(tasks))  #通过asyncio.wait()整合多个task
 
loop.run_until_complete(asyncio.gather(tasks))  #通过asyncio.gather()整合多个task
 
loop.run_until_complete(task_1)  #单个任务则不需要整合
 
loop.run_forever()  #但是这个方法在新版本已经取消,不再推荐使用,因为使用起来不简洁
 
'''
使用gather或者wait可以同时注册多个任务,实现并发,但他们的设计是完全不一样的,在前面的2.1.(4)中已经讨论过了,主要区别如下:
(1)参数形式不一样
gather的参数为 *coroutines_or_futures,即如这种形式
      tasks = asyncio.gather(*[task1,task2,task3])或者
      tasks = asyncio.gather(task1,task2,task3)
      loop.run_until_complete(tasks)
wait的参数为列表或者集合的形式,如下
      tasks = asyncio.wait([task1,task2,task3])
      loop.run_until_complete(tasks)
(2)返回的值不一样
gather的定义如下,gather返回的是每一个任务运行的结果,
      results = await asyncio.gather(*tasks) 
wait的定义如下,返回dones是已经完成的任务,pending是未完成的任务,都是集合类型
 done, pending = yield from asyncio.wait(fs)
(3)后面还会讲到他们的进一步使用

  简单来说:async.wait会返回两个值:done和pending,done为已完成的协程Task,pending为超时未完成的协程Task,需通过future.result调用Task的result。而async.gather返回的是已完成Task的result。

  第四步:关闭事件循环

loop.close()
 
'''
以上示例都没有调用 loop.close,好像也没有什么问题。所以到底要不要调 loop.close 呢?
简单来说,loop 只要不关闭,就还可以再运行:
loop.run_until_complete(do_some_work(loop, 1))
loop.run_until_complete(do_some_work(loop, 3))
loop.close()
但是如果关闭了,就不能再运行了:
loop.run_until_complete(do_some_work(loop, 1))
loop.close()
loop.run_until_complete(do_some_work(loop, 3))  # 此处异常
建议调用 loop.close,以彻底清理 loop 对象防止误用

  2,Python3.7版本

  在最新的python3.7版本中,asyncio又引进了一些新的特性和API,

  例子以:无参数,无返回值

# 3.7版本或之后 无参数无返回值 start
import asyncio
import time
 
 
async def hello1():
    print("Hello world 01 begin")
    await asyncio.sleep(3)  #模拟耗时任务3秒
    print("Hello again 01 end")
 
async def hello2():
    print("Hello world 02 begin")
    await asyncio.sleep(2)   #模拟耗时任务2秒
    print("Hello again 02 end")
 
async def hello3():
    print("Hello world 03 begin")
    await asyncio.sleep(4)   #模拟耗时任务4秒
    print("Hello again 03 end")
 
async def main():
    results=await asyncio.gather(hello1(),hello2(),hello3())
    for result in results:
        print(result)     #因为没返回值,故而返回None
 
asyncio.run(main())


# 3.7版本或之后 无参数无返回值 end

  输出如下

Hello world 01 begin
Hello world 02 begin
Hello world 03 begin
Hello again 02 end
Hello again 01 end
Hello again 03 end
None
None
None

  例子二:有参数有返回值

# 3.7版本或之后 有参数有返回值 start
import asyncio
import time
 
 
async def hello1(a,b):
    print("Hello world 01 begin")
    await asyncio.sleep(3)  #模拟耗时任务3秒
    print("Hello again 01 end")
    return a+b
 
async def hello2(a,b):
    print("Hello world 02 begin")
    await asyncio.sleep(2)   #模拟耗时任务2秒
    print("Hello again 02 end")
    return a-b
 
async def hello3(a,b):
    print("Hello world 03 begin")
    await asyncio.sleep(4)   #模拟耗时任务4秒
    print("Hello again 03 end")
    return a*b
 
async def main():
    results=await asyncio.gather(hello1(10,5),hello2(10,5),hello3(10,5))
    for result in results:
        print(result)
 
asyncio.run(main())

# 3.7版本或之后 有参数有返回值 end

  输出如下

Hello world 01 begin
Hello world 02 begin
Hello world 03 begin
Hello again 02 end
Hello again 01 end
Hello again 03 end
15
5
50

  (3)总结:两步走(针对3.7和以后版本)

  第一步:构建一个入口函数main  

  它也是一个异步协程函数,即通过async定义,并且要在main函数里面await一个或多个协程,和前面一样,我们可以通过gather或者是wait进行组合,对于有返回值的协程函数,一般就在main里面进行结果的获取。

  第二步:启动主函数main

  这是python3.7新添加的函数,就一句话,即

  asyncio.run(main())

  注意:

  不再需要显式的创建事件循环,因为在启动run函数的时候,就会自动创建一个新的事件循环。而且在main中也不需要通过事件循环去掉用被包装的协程函数,只需要向普通函数那样调用即可 ,只不过使用了await关键字而已。

  四,协程编程的优点

  1、无cpu分时切换线程保存上下文问题(协程上下文怎么保存)

  2、遇到io阻塞切换(怎么实现的)

  3、无需共享数据的保护锁(为什么)

  4、系列文章下篇预告——介绍低层的API,事件循环到底是怎么实现的以及future类的实现。



  



 

posted @ 2021-10-16 10:11  minseo  阅读(1434)  评论(1编辑  收藏  举报