Detailed explanation of Python's coroutine asynchronous IO (asyncio)

1. Introduction to coroutines

1.1 Definition

Coroutines are not system-level threads. Coroutines are often called "lightweight threads", "micro-threads", "fibers", etc. To put it simply, coroutines can be thought of as different functions in a thread, and these functions can quickly switch between each other.

Coroutines are very close to user-mode threads. Switching between user-mode threads does not need to be trapped in the kernel. However, switching between user-mode threads in some operating systems requires the assistance of kernel-mode threads.

Coroutines are features provided by programming languages ​​(or libs) (the switching method and process between coroutines can be determined by programmers), and are user-mode operations. Coroutines are suitable for IO-intensive tasks. Common languages ​​that provide native coroutine support include: c++20, golang, python, etc. Other languages ​​provide coroutine functions in the form of libraries, such as Tencent's fiber and libco before C++20, etc.

1.2 Classification 

There are two types of coroutines, one without stack , represented by asyncio in python , and one with stack , represented by gevent in python . This article mainly explains asyncio threads.

Thread with stack

Stackless thread

Remark

example:

lua thread

python vent

C# yield return

C# async\await

python asyncio

none

Whether to have a separate context:

yes no Context includes registers, stack frames

Local variable storage location:

stack heap Local variables of stackless coroutines are stored on the heap, such as generator data members.

advantage:

1. Each coroutine has a separate context, and this coroutine can be suspended anywhere in any nested function.

2. No compiler is required for syntax support, it can be implemented through assembly instructions

1. There is no need to save a separate context for each coroutine, and the memory usage is low.

2. Low switching cost and higher performance.

none

shortcoming:

1. A certain size of heap memory needs to be allocated in advance to save each coroutine context, so memory waste or stack overflow will occur.

2. The cost of context copying and switching is high, and the performance is lower than that of stackless coroutines.

1. The compiler needs to provide semantic support, such as C# yield return syntax sugar.

2. This coroutine can only be suspended within this generator, and cannot be suspended in nested functions.

3. Keywords are contagious to a certain extent, and asynchronous codes must have corresponding keywords. In contrast, stacked coroutines only need to make corresponding function calls.

A stackless coroutine cannot suspend this coroutine in a nested function. Since the stacked coroutine is implemented by saving and switching contexts including registers and execution stacks, the coroutine can be yielded and awakened inside the nested function of the coroutine function. .

2. Detailed explanation of python’s asyncio coroutine

2.1 Introduction

asyncio is a library for writing concurrent code using async/await syntax.

asyncio is used as the basis for multiple asynchronous frameworks that provide high-performance Python, including network and website services, database connection libraries, distributed task queues, and more.

asyncio is often the best choice for building IO-intensive and highly structured network code.

asyncio provides a set of high-level APIs for:

  • Run Python coroutines concurrently and gain full control over their execution;
  • Perform network IO and IPC;
  • Control child processes;
  • Implement distributed tasks through queues;
  • Synchronize concurrent code;
  • Create and manage event loops to provide asynchronous APIs for networking, running child processes, handling OS signals, and more;
  • Use transports to implement efficient protocols;
  • Bridge callback-based libraries and code with async/await syntax.

2.2 Use of asyncio coroutine (using python3.8 syntax)

Source code address of asyncio function: https://github.com/python/cpython/tree/3.8/Lib/asyncio

1) Coroutines are declared through async/await syntax, which is the recommended way to write asyncio applications.

The asyncio.run() function is used to run the top-level entry point "main()" function.

The asyncio.sleep(delay, result=None, *, loop=None) function is used to block for the specified number of seconds.

# coding=utf8

import sys
import asyncio

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

asyncio.run(main())

2) Event loop function (including creation, running and stopping of the loop)

The asyncio.get_running_loop() function returns the running event loop in the current OS thread.
The asyncio.get_event_loop() function gets the current event loop.
The asyncio.set_event_loop(loop) function sets loop to the current event loop of the current OS thread.
The asyncio.new_event_loop() function creates a new event loop.
The loop.run_until_complete(future) function runs until the future (an instance of Future) is completed.
The loop.run_forever() function runs the event loop until stop() is called.
The loop.stop() function stops the event loop.
The loop.is_running() function returns True if the event loop is currently running.
The loop.is_closed() function returns True if the event loop has been closed.
The loop.close() function closes the event loop.
The loop.create_future() function creates an asyncio.Future object attached to the event loop.
The loop.create_task(coro, *, name=None) function schedules the execution of a coroutine. Returns a Task object.
The loop.set_task_factory(factory) function sets a task factory, used in loop.create_task().
The loop.get_task_factory() function returns a task factory, or None if the default value is used.

 Example 1:

# coding=utf8

import sys
import asyncio

async def fun1():
    await asyncio.sleep(1)
    print('协程1')

async def fun2():
    await asyncio.sleep(1)
    print('协程2')

loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait([fun1(), fun2()]))
loop.close()

Example 2:

# coding=utf8

import sys
import asyncio
import time

# 一个对future进行赋值的函数
async def slow_operation(future, num):
    await asyncio.sleep(1)
    # 给future赋值
    future.set_result('Future'+ str(num) +' is done!')

def main():
    loop = asyncio.get_event_loop()
    # 创建一个future
    future1 = loop.create_future()
    # 使用ensure_future 创建Task
    asyncio.ensure_future(slow_operation(future1, 1))

    future2 = loop.create_future()
    asyncio.ensure_future(slow_operation(future2, 2))

    # gather Tasks,并通过run_uniti_complete来启动、终止loop
    loop.run_until_complete(asyncio.gather(future1, future2))

    print(future1.result())
    print(future2.result())

    loop.close()

if __name__ == "__main__":
    main()
    

3) Scheduling callbacks and delayed callbacks 

The loop.call_soon(callback, *args, context=None) function arranges for callback to be called with the args argument on the next iteration of the event loop. Callbacks are called in the order they were registered. Each callback is called only once. Method is not thread safe.
The loop.call_soon_threadsafe(callback, *args, context=None) function is a thread-safe variant of call_soon(). Must be used to schedule callbacks from other threads.
The loop.call_later(delay, callback, *args, context=None) function arranges for callback to be called after a given delay seconds (can be int or float).
The loop.call_at(when, callback, *args, context=None) function schedules the callback to be called at the given absolute timestamp (an int or float), using the same time reference as loop.time().
The loop.time() function returns the current time as a float value based on the monotonic clock inside the time loop.

# coding=utf8

import sys
import asyncio
from threading import Thread
import time

def callback(arg, loop):
    print('回调函数arg={} 回调的时间time={}'.format(arg, loop.time()))

async def task(loop):
    now = loop.time()
    print('时钟时间:{}'.format(time.time()))
    print('时事件循环时间:{}'.format(loop.time()))
    print('注册回调函数')
    loop.call_at(now + 1, callback, 'call_at1', loop) # 等待1秒执行 call_at 函数
    loop.call_at(now + 2, callback, 'call_at2', loop)
    loop.call_later(3, callback, 'call_later1', loop) # 等待3秒执行 call_later 函数
    loop.call_later(4, callback, 'call_later2', loop)
    loop.call_soon(callback, 'call_soon', loop) # 立即执行执行 call_soon 函数
    await asyncio.sleep(4)

def main():
    event_loop = asyncio.get_event_loop()
    try:
        print('进入事件循环监听')
        event_loop.run_until_complete(task(event_loop))  # 将事件循环对象传入task函数中
    finally:
        print('关闭事件循环监听')
        event_loop.close()

if __name__ == "__main__":
    main()

4) Socket connection and Streams function

  • loop.create_connection(protocol_factory, host=None, port=None, *, ssl=None, family=0, proto=0, flags=0, sock=None, local_addr=None, server_hostname=None, ssl_handshake_timeout=None, happy_eyeballs_delay= None, interleave=None) function opens a streaming connection to the address specified by host and port.
  • loop.create_server(protocol_factory, host=None, port=None, *, family=socket.AF_UNSPEC, flags=socket.AI_PASSIVE, sock=None, backlog=100, ssl=None, reuse_address=None, reuse_port=None, ssl_handshake_timeout= None, start_serving=True) The function creates a TCP service (socket type SOCK_STREAM) that listens to the port port of the host address.
  • The loop.create_unix_server(protocol_factory, path=None, *, sock=None, backlog=100, ssl=None, ssl_handshake_timeout=None, start_serving=True) function is similar to loop.create_server() but is specific to the AF_UNIX socket family. path is the required Unix domain socket name, unless the sock parameter is provided. Abstract Unix sockets, str, bytes and Path are all supported.
  • The loop.connect_accepted_socket(protocol_factory, sock, *, ssl=None, ssl_handshake_timeout=None) function wraps the accepted connection into a transport/protocol pair.
  • The loop.sock_recv(sock, nbytes) function receives up to nbytes from sock. Asynchronous version of socket.recv().
  • The loop.sock_recv_into(sock, buf) function receives data from sock and puts it into the buf buffer. Mimics the blocking socket.recv_into() method.
  • The loop.sock_sendall(sock, data) function sends data to the sock socket. Asynchronous version of socket.sendall().
  • The loop.sock_accept(sock) function accepts a connection. Mimics the blocking socket.accept() method.
  • The loop.sock_sendfile(sock, file, offset=0, count=None, *, fallback=True) function sends files using the high-performance os.sendfile when possible. Returns the total number of bytes sent.
  • asyncio.open_connection(host=None, port=None, *, loop=None, limit=None, ssl=None, family=0, proto=0, flags=0, sock=None, local_addr=None, server_hostname=None, ssl_handshake_timeout=None) function establishes a network connection and returns a pair of (reader, writer) objects.

  • asyncio.start_server(client_connected_cb, host=None, port=None, *, loop=None, limit=None, family=socket.AF_UNSPEC, flags=socket.AI_PASSIVE, sock=None, backlog=100, ssl=None, reuse_address= None, reuse_port=None, ssl_handshake_timeout=None, start_serving=True) function starts the socket service.

  • asyncio.open_unix_connection(path=None, *, loop=None, limit=None, ssl=None, sock=None, server_hostname=None, ssl_handshake_timeout=None) function establishes a Unix socket connection and returns (reader, writer) this to the return value. Similar to open_connection(), but operates on Unix sockets.

  • The asyncio.start_unix_server(client_connected_cb, path=None, *, loop=None, limit=None, sock=None, backlog=100, ssl=None, ssl_handshake_timeout=None, start_serving=True) function starts a Unix socket service . Similar to start_server(), but operates on a Unix socket.

  • The asyncio.StreamReader class represents a reader object that provides an API to read data from an IO stream.

    The reader.read(n=-1) function reads n bytes. If n is not set, it is automatically set to -1, reads to EOF and returns all read bytes.
    The reader.readline() function reads a line, where "line" refers to a sequence of bytes ending with \n. If EOF is read and no \n is found, this method returns the partially read data. If EOF is read and the internal buffer is empty, an empty bytes object is returned.
    The reader.readexactly(n) function reads n bytes accurately, no more or less.
    reader.readuntil(separator=b'\n') The function reads data from the stream until a separator is encountered. After success, the data and the specified separator will be deleted (or consumed) from the internal buffer. The returned data will include the specified separator at the end. If the amount of data read exceeds the configured stream limit, a LimitOverrunError exception will be raised and the data will remain in the internal buffer and can be read again. If EOF is reached before a complete separator is found, an IncompleteReadError exception is raised and the internal buffer is reset. The IncompleteReadError.partial property may contain part of the specified separator.
    The reader.at_eof() function returns True if the buffer is empty and feed_eof() is called.

  • The asyncio.StreamWriter class represents a writer object that provides an API to write data to an IO stream.

    The writer.write(data) function will try to write data to the underlying socket immediately. If the write fails, the data is queued into the internal write buffer until it can be sent.
    The writer.writelines(data) function immediately attempts to write a list of bytestrings (or any iterable object) to the underlying socket. If the write fails, the data is queued into the internal write buffer until it can be sent.
    The writer.close() function closes the stream and underlying socket.
    The writer.can_write_eof() function returns True if the underlying transport supports the write_eof() method, otherwise it returns False.
    The writer.write_eof() function closes the write end of the stream after the buffered write data has been flushed.
    The writer.transport() function returns the underlying asyncio transport.
    The writer.drain() function waits until writing to the stream can be resumed appropriately.
    The writer.is_closing() function returns True if the stream has been closed or is being closed.
    The writer.wait_closed() function waits until the stream is closed.

server code: 

# coding=utf8

import asyncio
from asyncio import StreamReader, StreamWriter


async def echo(reader: StreamReader, writer: StreamWriter):
    data = await reader.read(1024)
    message = data.decode()
    addr = writer.get_extra_info('peername')

    print(f"Received {message} from {addr}")
    print(f"Send: {message}")

    writer.write(data)
    await writer.drain()

    writer.close()


async def main(host, port):
    server = await asyncio.start_server(echo, host, port)
    addr = server.sockets[0].getsockname()
    print(f'Serving on {addr}')
    async with server:
        await server.serve_forever()

asyncio.run(main("127.0.0.1", 9999))

client code:

# coding=utf8

import asyncio


async def tcp_echo_client(message):
    reader, writer = await asyncio.open_connection('127.0.0.1', 9999)
    print(f'Send to server: {message}')

    writer.write(message.encode())
    await writer.drain()

    data = await reader.read(1024)
    print(f'Received from server: {data.decode()}')

    writer.close()
    await writer.wait_closed()


if __name__ == '__main__':
    while True:
        send_msg = input("send: ")
        asyncio.run(tcp_echo_client(send_msg))

5) Execute code in a thread or process pool

The loop.run_in_executor(executor, func, *args) function arranges to call func in the specified executor.

# coding=utf8

import asyncio
import concurrent.futures

def blocking_io():
    # File operations (such as logging) can block the
    # event loop: run them in a thread pool.
    with open('/dev/urandom', 'rb') as f:
        return f.read(100)

def cpu_bound():
    # CPU-bound operations will block the event loop:
    # in general it is preferable to run them in a
    # process pool.
    return sum(i * i for i in range(5))

async def main():
    loop = asyncio.get_running_loop()

    ## Options:

    # 1. Run in the default loop's executor:
    result = await loop.run_in_executor(
        None, blocking_io)
    print('default thread pool', result)
    print("\n")

    # 2. Run in a custom thread pool:
    with concurrent.futures.ThreadPoolExecutor() as pool:
        result = await loop.run_in_executor(
            pool, blocking_io)
        print('custom thread pool', result)
        print("\n")

    # 3. Run in a custom process pool:
    with concurrent.futures.ProcessPoolExecutor() as pool:
        result = await loop.run_in_executor(
            pool, cpu_bound)
        print('custom process pool', result)

asyncio.run(main())



6) The asyncio.create_task(coro, *, name=None) function is used to package a coroutine into a Task, schedule it for execution, and return the Task object.

# coding=utf8

import sys
import asyncio
import time

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

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')}")

asyncio.run(main())

7) Error handling API

The loop.set_exception_handler(handler) function sets handler as the new event loop exception handler.
The loop.get_exception_handler() function returns the current exception handler, or None if no exception handler is set.
loop.default_exception_handler(context) function 's default exception handler.
The loop.call_exception_handler(context) function calls the exception handler of the current event loop.
The loop.get_debug() function obtains the event loop debug mode setting (bool).
The loop.set_debug(enabled: bool) function sets the debug mode of the event loop.

# coding=utf8

import sys
import asyncio

def handle_exception(loop, context): 
    print('Error:', context['message']) 
 
async def my_task(): 
    await asyncio.sleep(1)
    print('task1')
 
loop = asyncio.get_event_loop() 
loop.set_exception_handler(handle_exception) 
loop.run_until_complete(my_task()) 
loop.close()

8)Future 

The asyncio.Future(*, loop=None) function is a Future representing the final result of an asynchronous operation. Not thread safe.
The asyncio.isfuture(obj) function is used to determine if obj is an example of the asyncio.Future class, an instance of the asyncio.Task class, or an object with the _asyncio_future_blocking attribute, and returns True.
The asyncio.ensure_future(obj, *, loop=None) function creates a new task.
The asyncio.wrap_future(future, *, loop=None) function encapsulates a concurrent.futures.Future object into an asyncio.Future object.

Future object related functions:

The fut.result() function returns the result of a Future.
The fut.set_result(result) function marks the Future as complete and sets the result.
The fut.set_exception(exception) function marks the Future as complete and sets an exception.
The fut.done() function returns True if the Future is completed.
The fut.cancelled() function returns True if the Future has been canceled.
The fut.add_done_callback(callback, *, context=None) function adds a callback function that runs when the Future is completed.
The fut.remove_done_callback(callback) function removes callback from the callback list.
The fut.cancel() function cancels the Future and dispatches the callback function.
The fut.exception() function returns the Future's set exception.
The fut.get_loop() function returns the event loop to which the Future object has been bound.

# coding=utf8

import sys
import asyncio
import time

# 定义一个协程
async def slow_operation(fut):
    await asyncio.sleep(1)
    fut.set_result(22)

def def_callback(fut):
    number = fut.result()
    print(number + 1)

def main():
    # 获得全局循环事件
    loop = asyncio.get_event_loop()

    # 实例化期物对象
    fut = asyncio.Future()
    asyncio.ensure_future(slow_operation(fut))

    # 执行回调函数
    fut.add_done_callback(def_callback)

    # loop 的 run_until_complete 会将 _run_until_complete_cb 添加到 future 的完成回调列表中。而 _run_until_complete_cb 中会执行 loop.stop() 方法
    loop.run_until_complete(fut)

    # 关闭事件循环对象
    loop.close()

if __name__ == "__main__":
    main()

9) The asyncio.gather(*aws, loop=None, return_exceptions=False) function is used to concurrently run waitable objects in the aws sequence. If a waitable object in AWS is a coroutine, it will automatically be added to the schedule as a task.

# coding=utf8

import sys
import asyncio

async def factorial(name, number):
    f = 1
    for i in range(2, number + 1):
        print(f"Task {name}: Compute factorial({i})...")
        await asyncio.sleep(1)
        f *= i
    print(f"Task {name}: factorial({number}) = {f}")

async def main():
    # Schedule three calls *concurrently*:
    await asyncio.gather(
        factorial("A", 2),
        factorial("B", 3),
        factorial("C", 4),
    )

asyncio.run(main())

10) The asyncio.shield(aw, *, loop=None) function is used to protect a waitable object from being canceled.

# coding=utf8

import sys
import asyncio

async def task_func(number):
    await asyncio.sleep(1)
    print('函数执行成功:'+str(number))
 
async def cancel_task(task):
    await asyncio.sleep(0.2)
    was_cancelled = task.cancel()
    print(f'cancelled: {was_cancelled}')
 
async def main():
    coro = task_func(1)
    task = asyncio.create_task(coro)
    shielded = asyncio.shield(task)
    asyncio.create_task(cancel_task(shielded))
    try:
        result = await shielded
        print(f'>got: {result}')
    except asyncio.CancelledError:
        print('shielded was cancelled')

    await asyncio.sleep(1)
    print(f'shielded: {shielded}')
    print(f'task: {task}')
 
asyncio.run(main())

11) The asyncio.wait(aws, *, loop=None, timeout=None, return_when=ALL_COMPLETED) function concurrently runs the waitable objects in the aws iterable object and enters the blocking state until the conditions specified by return_when are met.

return_when specifies when this function should return. It must be one of the following constants:

constant

describe

FIRST_COMPLETED

The function will return when any waitable object ends or is cancelled.

FIRST_EXCEPTION

The function will return when any awaitable object ends by throwing an exception. It is equivalent when no exception is thrown  ALL_COMPLETED.

ALL_COMPLETED

The function will return when all waitable objects have ended or been cancelled.

# coding=utf8

import sys
import asyncio

async def coroutine_example(name):
    print('正在执行name:', name)
    await asyncio.sleep(1)
    print('执行完毕name:', name)

loop = asyncio.get_event_loop()

tasks = [coroutine_example('Zarten_' + str(i)) for i in range(3)]
wait_coro = asyncio.wait(tasks)
loop.run_until_complete(wait_coro)
loop.close()

12) The asyncio.wait_for(aw, timeout, *, loop=None) function waits for the aw waitable object to complete and times out after the specified timeout seconds.

# coding=utf8

import sys
import asyncio

async def eternity():
    # Sleep for one hour
    await asyncio.sleep(3600)
    print('yay!')

async def main():
    # Wait for at most 1 second
    try:
        await asyncio.wait_for(eternity(), timeout=1)
    except asyncio.TimeoutError:
        print('timeout!')

asyncio.run(main())

13) The asyncio.run_coroutine_threadsafe(coro, loop) function submits a coroutine to the specified event loop. Thread safe.

# coding=utf8

import sys
import asyncio
import threading

async def main(i): 
    while True:
        await asyncio.sleep(1)
        print(i)
 
async def production_task():
    for i in range(1, 4):
        # 将不同参数main这个协程循环注册到运行在线程中的循环,
        # thread_loop会获得一循环任务
        asyncio.run_coroutine_threadsafe(main(i),thread_loop)
        # 注意:run_coroutine_threadsafe 这个方法只能用在运行在线程中的循环事件使用
 
def start_loop(thread_loop):
     #  运行事件循环, loop以参数的形式传递进来运行
    asyncio.set_event_loop(thread_loop)
    thread_loop.run_forever()
 
if __name__ == '__main__':
    
    # 获取一个事件循环
    thread_loop = asyncio.new_event_loop()
    # 将次事件循环运行在一个线程中,防止阻塞当前主线程,运行线程,同时协程事件循环也会运行
    threading.Thread(target=start_loop, args=(thread_loop,)).start()
    
    # 将生产任务的协程注册到这个循环中
    loop = asyncio.get_event_loop()
    # 运行次循环
    loop.run_until_complete(production_task())

14) The asyncio.current_task(loop=None) function returns the currently running Task instance, or None if there is no running task. If loop is None, get_running_loop() will be used to obtain the current event loop.

The asyncio.all_tasks(loop=None) function returns a collection of unfinished Task objects run by the event loop. If loop is None, get_running_loop() will be used to obtain the current event loop.

# coding=utf8

import sys
import asyncio
import time

async def my_coroutine():
    task = asyncio.current_task()
    print(task)

async def main():
    task1 = asyncio.create_task(my_coroutine())
    task2 = asyncio.create_task(my_coroutine())
    tasks = [task1, task2]
    await asyncio.gather(*tasks)

asyncio.run(main())

15) asyncio.Task(coro, *, loop=None, name=None) function is an object similar to Future, which can run Python coroutines. Not thread safe. 

Task object related functions:

The task.cancelled() function returns True if the Task object is canceled.
The task.done() function returns True if the Task object has completed.
The task.result() function returns the result of Task.
The task.exception() function returns the exception of the Task object.
The task.remove_done_callback(callback) function removes callback from the callback list.
The task.get_stack(*, limit=None) function returns the stack frame list of this Task object.
task.print_stack(*, limit=None, file=None) function prints the stack or traceback of this Task object.
The task.get_coro() function returns the coroutine object wrapped by Task.
The task.get_name() function returns the name of the Task.
The task.set_name(value) function sets the name of the Task.

# coding=utf8

import sys
import asyncio
import time

async def cancel_me():
    print('cancel_me(): before sleep')

    try:
        # Wait for 1 hour
        await asyncio.sleep(3600)
    except asyncio.CancelledError:
        print('cancel_me(): cancel sleep')
        raise
    finally:
        print('cancel_me(): after sleep')

async def main():
    # Create a "cancel_me" Task
    task = asyncio.create_task(cancel_me())

    # Wait for 1 second
    await asyncio.sleep(1)

    task.cancel()
    try:
        await task
    except asyncio.CancelledError:
        print("main(): cancel_me is cancelled now")

asyncio.run(main())

16) Generator-based coroutines (deprecated after Python 3.10)

Generator-based coroutines are the predecessor of async/await syntax. They are Python generators created using yield from statements that can await Futures and other coroutines.

@asyncio.coroutine Decorator used to mark generator-based coroutines.

The asyncio.iscoroutine(obj) function returns True if obj is a coroutine object.

The asyncio.iscoroutinefunction(func) function returns True if func is a coroutine function.

# coding=utf8

import sys
import asyncio
import time

@asyncio.coroutine # 标志协程的装饰器
def taskIO_1():
    print('开始运行IO任务1...')
    yield from asyncio.sleep(2)  # 假设该任务耗时2s
    print('IO任务1已完成,耗时2s')
    return taskIO_1.__name__

@asyncio.coroutine # 标志协程的装饰器
def taskIO_2():
    print('开始运行IO任务2...')
    yield from asyncio.sleep(3)  # 假设该任务耗时3s
    print('IO任务2已完成,耗时3s')
    return taskIO_2.__name__

asyncio.run(taskIO_1())
asyncio.run(taskIO_2())

Guess you like

Origin blog.csdn.net/m0_68949064/article/details/132805165