- 自学 python 中的异步编程 asyncio (一):学习基本概念
- 自学 python 中的异步编程 asyncio (二):asyncio模块与核心组件
- 自学 python 中的异步编程 asyncio (三):asyncio 实现基本异步编程
- 自学 python 中的异步编程 asyncio (四):基本的异步IO编程
- 自学 python 中的异步编程 asyncio (五):asyncio 与 线程thread
- 自学 python 中的异步编程 asyncio (六):高级异步编程
- 自学 python 中的异步编程 asyncio:实战(一)爬虫
基本的异步IO编程
异步IO编程的三大基本场景:
- 使用asyncio进行文件读写
- 使用asyncio进行网络编程
- 使用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可以方便地进行网络编程,例如实现TCP或UDP客户端和服务器。
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.StreamReader
和asyncio.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()
函数将多个协程同时执行。 - 整个程序的执行过程如下:
- 创建事件循环对象
func1()
和func2()
。 - 执行
main()
协程。 main()
协程启动func1()
和func2()
协程,并等待它们都运行完毕。func1()
和func2()
协程执行一部分代码后,暂停1秒。- 1秒后,
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 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秒
"""
问题解读
- 这两种写法有什么区别吗?
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())
是最简单的方法。 - 但是如果你需要在一个程序中同时运行多个异步任务,使用传统的方式会更加灵活,因为你可以手动管理事件循环的创建和关闭。