自学 python 中的异步编程 asyncio (四):基本的异步IO编程

文章介绍了Python的asyncio模块在异步IO编程中的应用,包括基本概念、文件读写、网络编程(TCP服务器)、串行和并行任务调度。示例展示了如何使用asyncio进行文件操作和建立TCP服务器,以及如何通过await和asyncio.gather()进行任务调度。此外,还提到了在多线程中使用事件循环的场景。

基本的异步IO编程

异步IO编程的三大基本场景:

  1. 使用asyncio进行文件读写
  2. 使用asyncio进行网络编程
  3. 使用asyncio进行串行和并行任务调度

1 使用asyncio进行文件读写

  • 在Python中,我们可以使用asyncio模块来进行异步IO编程,它提供了一套完整的异步IO编程框架
  • Python3.10版本的asyncio模块已经没有open_file方法了,因此在代码中调用asyncio.open_file()方法时,会提示AttributeError
  • 可以考虑使用aiofiles库来进行文件读写操作。
  • aiofiles库是一个第三方库,提供了异步IO的文件读写操作,与asyncio库配合使用非常方便。
使用aiofiles库进行文件读写
import asyncio
import aiofiles


async def read_file(file_path):
    async with aiofiles.open(file_path, mode='r') as f:
        content = await f.read()
        print(f'Read file content: {content}')


async def write_file(file_path, content):
    async with aiofiles.open(file_path, mode='w') as f:
        await f.write(content)
        print(f'Write file success!')


async def main():
    file_path = './test.txt'
    await write_file(file_path, 'Hello, asyncio and aiofiles!')
    await read_file(file_path)


if __name__ == '__main__':
    asyncio.run(main())

2 使用asyncio进行网络编程

  • 在异步IO编程中,网络编程也是一个非常常见的应用场景。
  • 使用asyncio可以方便地进行网络编程,例如实现TCPUDP客户端和服务器。
asyncio.start_server() : TCP服务器

下面是一个简单的例子,用于使用asyncio实现一个TCP服务器

  • 主要思路为创建一个服务器并等待客户端连接,一旦有连接则创建一个协程来处理客户端请求。
  • 处理客户端请求的函数handle_client首先从客户端读取数据,然后将数据发送回客户端,并在控制台输出相关信息。
  • 最后关闭客户端连接。
  • 主函数main中启动服务器并等待客户端连接。
import asyncio

# 1 使用async/await 定义协程
async def handle_client(reader, writer):
    # 从客户端读取数据
    data = await reader.read(1024)
    message = data.decode()
    # 获取客户端地址
    addr = writer.get_extra_info('peername')
    print(f"Received {message!r} from {addr!r}")
    # 将收到的数据发送回客户端
    writer.write(data)
    # 强制数据发送并清空缓冲区
    await writer.drain()
    print(f"Send: {message!r}")
    writer.close()

# 1 使用async/await 定义协程
async def main():
    # 使用asyncio.start_server()函数创建一个TCP服务器 server
    # 指定要监听的IP地址和端口号,并将handle_client()函数作为处理器
    server = await asyncio.start_server(handle_client, '127.0.0.1', 8888)
    # 获取服务器地址
    addr = server.sockets[0].getsockname()
    print(f'Serving on {addr}')
    
    # 使用async with语句来运行TCP服务器,以便在退出时自动关闭它。
    async with server:
        await server.serve_forever()


if __name__ == "__main__":
    # 2 使用 asyncio.run 创建事件循环
    # 3 在事件循环 asyncio.run 中执行协程
    asyncio.run(main())

  • 在这个例子中,我们定义了一个handle_client函数,它接受一个reader和一个writer,这两个对象都是asyncio.StreamReaderasyncio.StreamWriter类型的。

  • 我们首先从reader中读取数据,然后将其返回给客户端。在这个过程中,我们使用了await关键字来等待操作完成。

  • 接下来,我们定义了一个main函数,它启动了一个TCP服务器,绑定到本地地址127.0.0.1的8888端口上。

  • 我们使用await asyncio.start_server()函数来启动服务器,然后使用await server.serve_forever()函数来让服务器一直运行。

  • 最后,我们使用asyncio.run()函数来运行main函数。这个函数会启动事件循环,并在事件循环中运行main函数。

  • 可以通过telnet来测试这个TCP服务器。

    • 例如,我们可以运行以下命令:
telnet 127.0.0.1 8888

然后我们可以输入一些文本,它会被原样返回。当我们结束telnet会话时,服务器将关闭连接。

这个例子只是一个简单的TCP服务器示例,但是它展示了如何使用asyncio进行网络编程。在实际应用中,我们可以根据需要自定义更加复杂的网络通信协议和逻辑。

3 使用asyncio进行串行和并行任务调度

当我们需要在异步编程中执行多个协程时,我们可以使用 asyncio 提供的任务调度工具,实现串行并行任务的执行。

1 await 串行任务调度
  • 串行任务调度:将多个协程按照一定的顺序串联起来执行,即等待一个协程执行完毕后再执行下一个协程。
    • 我们可以使用 await 关键字将多个协程串联起来执行。
    • 在这个例子中,我们先执行 func1() 协程,等待它执行完毕后再执行 func2() 协程
import asyncio
import time


async def fun1():
    start_time = time.time()
    await asyncio.sleep(2)
    print(f"func1: {time.time() - start_time}s")


async def fun2():
    start_time = time.time()
    await asyncio.sleep(2)
    print(f"func2: {time.time() - start_time}s")


async def main():
    start = time.time()
    await fun1()
    await fun2()
    print(time.time() - start)


asyncio.run(main())
2 asyncio.gather() 并行任务调度
  • 并行任务调度:将多个协程同时执行,不需要等待一个协程执行完毕后再执行下一个协程。
  • 我们可以使用 asyncio.gather() 函数将多个协程同时执行。
  • 整个程序的执行过程如下:
    1. 创建事件循环对象func1()func2()
    2. 执行 main() 协程。
    3. main() 协程启动 func1()func2() 协程,并等待它们都运行完毕。
    4. func1()func2() 协程执行一部分代码后,暂停1秒。
    5. 1秒后,func1()func2() 协程继续执行剩余的代码。
    6. 当所有协程都运行完毕后,程序退出事件循环。
import asyncio
import time


async def fun1():
    start_time = time.time()
    await asyncio.sleep(2)
    print(f"func1: {time.time() - start_time}s")


async def fun2():
    start_time = time.time()
    await asyncio.sleep(2)
    print(f"func2: {time.time() - start_time}s")


async def main():
    start = time.time()
    await asyncio.gather(fun2(), fun1())
    print(time.time() - start)


asyncio.run(main())

  • 需要注意的是,如果一个协程中有 I/O 操作,比如网络请求文件读写,那么在这个协程执行过程中,事件循环可以切换到其他协程中执行,提高程序的执行效率。

  • 同时,如果一个协程在执行过程中出现了阻塞,比如等待一个锁或者等待一个慢速的 I/O 操作,那么事件循环会自动切换到其他协程中执行,避免程序的执行被阻塞。

4 更复杂的情况

  • 另外创建一条线程,在线程创建一个一直循环的“事件循环”
import asyncio
import time
from threading import Thread


def thread_new_loop(new_loop):  # 一个将被丢进线程的函数
    asyncio.set_event_loop(new_loop)  # 调用loop需要使用set_event_loop方法指定loop
    new_loop.run_forever()  # run_forever() 会永远阻塞当前线程,直到有人停止了该loop为止。


async def async_function(num):  # 一个协程
    await asyncio.sleep(num)
    print('异步任务{}花费时间:{}秒'.format(num, time.time() - now_time))
    return '异步任务{}完成时间:{}秒'.format(num, time.time() - now_time)


now_time = time.time()  # 程序运行时的时间戳

# 创建一个新的loop
# get_event_loop()只会在主线程创建新的event loop,其他线程中调用 get_event_loop() 则会报错
new_loop = asyncio.new_event_loop()

t = Thread(target=thread_new_loop, args=(new_loop,))  # 创建线程
t.start()  # 启动线程

# 调用asyncio.run_coroutine_threadsafe实现回调
even = asyncio.run_coroutine_threadsafe(async_function(1), new_loop)
# 当run_coroutine_threadsafe对象执行cancel()方法就会取消该任务事件(当速度够快有概率取消前已经执行)
even.cancel()

asyncio.run_coroutine_threadsafe(async_function(2), new_loop)
asyncio.run_coroutine_threadsafe(async_function(3), new_loop)

print('主进程运行花费时长:{}秒'.format(time.time() - now_time))


"""输出结果:
主进程运行花费时长:0.0009984970092773438秒
异步任务2花费时间:2.016122817993164秒
异步任务3花费时间:3.00211501121521秒
"""

问题解读

  1. 这两种写法有什么区别吗?
 loop = asyncio.get_event_loop()
 loop.run_until_complete(main())
 asyncio.run(main()) 

这两种写法都是用来运行异步程序的,但是有一些细微的区别。

  • asyncio.run(main()) 是 Python 3.7 引入的一种简单的运行异步代码的方法。
    • 它会自动创建一个新的事件循环并将其用于运行 main() 协程,然后在协程运行结束后自动关闭事件循环。
  • 相比之下,loop = asyncio.get_event_loop()loop.run_until_complete(main()) 是使用 asyncio 包的传统方式
    • get_event_loop() 用于获取当前的事件循环,如果没有则会创建一个新的事件循环
    • run_until_complete(main()) 则是运行 main() 协程直到协程结束,它使用的是已经存在的事件循环,因此可以在一个程序中运行多个异步任务
  • 综上所述,如果你的代码只包含一个异步任务,使用 asyncio.run(main()) 是最简单的方法。
  • 但是如果你需要在一个程序中同时运行多个异步任务,使用传统的方式会更加灵活,因为你可以手动管理事件循环的创建和关闭。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值