Python 进阶系列笔记文章链接:
Python 进阶学习笔记之一:内置常用类型及方法
Python 进阶学习笔记之二:常用数据类型(上)
Python 进阶学习笔记之三:常用数据类型(下)
Python 进阶学习笔记之四:高效迭代器工具
Python 进阶学习笔记之五:异步 IO
Python 进阶学习笔记之六:多线程编程
Python 进阶学习笔记之七:互联网支持
Python 进阶学习笔记之八:面向对象高级编程
Python 进阶学习笔记之九:IO 编程
Python 进阶学习笔记之十:一般加密支持
Python 进阶学习笔记之十一:日志支持
Python 进阶学习笔记之十二:数据压缩与归档
编程中,我们经常会遇到“并发”这个概念,目的是让软件能充分利用硬件资源,提高性能。并发的方式有多种,多线程,多进程,异步IO等。多线程和多进程更多应用于CPU密集型的场景,比如科学计算的时间都耗费在CPU上,利用多核CPU来分担计算任务。多线程和多进程之间的场景切换和通讯代价很高,不适合IO密集型的场景,而异步IO就是非常适合IO密集型的场景,比如网络爬虫和Web服务。
在计算机程序中,IO就是读写磁盘、读写网络的操作,这种读写速度比读写内存、CPU缓存慢得多,前者的耗时是后者的成千上万倍甚至更多。这就导致,IO密集型的场景99%以上的时间都花费在IO等待的时间上。异步IO就是把CPU从漫长的等待中解放出来的方法。这就可以大大提高我们写的软件系统的并发性。
asyncio 模块用于支持异步 IO 编程和并发编程,这个模块是从 3.4
版本引入的标准库,在后续的几个小版本中一直在增强,3.7 中也增加了一些高层次的 API,本文会略有介绍。关于并发编程,在任何高级语言中都是一门很深奥的知识分支,本笔记进作为入门知识,如果要深入研究,请参阅专门的专题或书籍。
Python 3.6 版本后 asyncio
模块已经代替了原来的 asyncore
模块(异步Sockt 处理)和 asynchat
模块(异步 Socket 指令),因此3.6+版本的 python 推荐使用此模块进行异步执行和异步通信的编码实现。
入门使用
在 Python 中,一个异步执行单元叫做 协程
,协程通过 async/await 语法进行声明,是编写异步应用的推荐方式。
简单示例:
import asyncio
async def main():
print('hello')
await asyncio.sleep(1)
print('world')
asyncio.run(main()) # 注意,上面的协程函数 main 直接调用并不会执行
要真正运行一个协程,asyncio 提供了三种主要机制:
asyncio.run()
函数用来运行最高层级的入口点 “main()” 函数 (参见上面的示例)await
来修饰一个协程,需要注意的是 await
只能用于函数内部,因此上面的例子如果要改用 await
的话,还需要稍加修改async def run_main():
await main()
asyncio.run(run_main())
asyncio.create_task()
函数用来并发运行多个协程import asyncio
async def foo(thread_name):
c = 1
while c < 5:
print(f"{thread_name}: {c}")
c = c + 1
await asyncio.sleep(1)
async def main_run():
t1 = asyncio.create_task(foo("t1"))
t2 = asyncio.create_task(foo("t2")) # 3.7+ 版本推荐使用 create_task 创建 task
t3 = asyncio.ensure_future(foo("t3")) # 3.7 以前版本使用此方法创建 task
await t1
await t2
await t3
asyncio.run(main_run())
task 的目的之一就是支持 并发 执行多个 协程,当一个协程通过 asyncio.create_task()
等函数被打包为一个 任务,该协程将自动排入日程准备立即运行。
上面列子中还需要关注的点:
asyncio.run(coro, *, debug=False)
:运行 asyncio 程序入口,如果 debug 参数为 True,事件循环将以调试模式运行。asyncio.create_task(coro)
:将 coro 协程 打包为一个 Task 排入日程准备执行。返回 Task 对象。此函数 在 Python 3.7 中被加入。在 Python 3.7 之前,可以改用低层级的 asyncio.ensure_future()
函数。asyncio.sleep(delay, result=None, *, loop=None)
: 阻塞 delay 指定的秒数,也就是 休眠 ,注意这个函数本身是个协程函数,需要使用 await
进行调用。并发运行任务
函数 asyncio.gather(*aws, loop=None, return_exceptions=False)
用来支持并发 task 的执行,第一个参数支持多个协程对象,如果所有可等待(协程)对象都成功完成,结果将是一个由所有返回值聚合而成的列表,结果值的顺序与 aws 中可等待对象的顺序一致。
示例:
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():
await asyncio.gather(
factorial("A", 2),
factorial("B", 3),
factorial("C", 4),
)
asyncio.run(main())
# 输出
#
# Task A: Compute factorial(2)...
# Task B: Compute factorial(2)...
# Task C: Compute factorial(2)...
# Task A: factorial(2) = 2
# Task B: Compute factorial(3)...
# Task C: Compute factorial(3)...
# Task B: factorial(3) = 6
# Task C: Compute factorial(4)...
# Task C: factorial(4) = 24
超时支持
有两种实现方式:
1.asyncio.wait_for(aw, timeout, *, loop=None)
:等待 aw 可等待对象 完成,指定 timeout 秒数后超时。timeout 可以为 None,也可以为 float 或 int 型数值表示的等待秒数。如果 timeout 为 None,则等待直到完成。如果发生超时,任务将取消并引发 asyncio.TimeoutError。需要特别注意的是,函数将等待直到目标对象确实被取消,所以总等待时间可能超过 timeout 指定的秒数。
```python
async def eternity():
await asyncio.sleep(3600)
print(‘yay!’)
async def main():
try:
await asyncio.wait_for(eternity(), timeout=1.0)
except asyncio.TimeoutError:
print('timeout!')
asyncio.run(main())
# 输出:
# timeout!
```
asyncio.wait(task, *, loop=None, timeout=None, return_when=ALL_COMPLETED)
:简单等待方式,并发运行指定的 task
并阻塞线程直到满足 return_when 指定的条件。async def foo():
return 42
task = asyncio.create_task(foo())
done, pending = await asyncio.wait({task})
if task in done
pass
return_when 指定此函数应在何时返回。它必须为以下常数之一:
常量 | 描述 |
---|---|
FIRST_COMPLETED | 函数将在任意可等待对象结束或取消时返回。 |
FIRST_EXCEPTION | 函数将在任意可等待对象因引发异常而结束时返回。当没有引发任何异常时它就相当于 ALL_COMPLETED。 |
ALL_COMPLETED | 函数将在所有可等待对象结束或取消时返回。 |
asyncio.as_completed(aws, *, loop=None, timeout=None)
:并发地运行 aws 集合中的 可等待对象。返回一个 Future 对象的迭代器。返回的每个 Future 对象代表来自剩余可等待对象集合的最早结果。如果在所有 Future 对象完成前发生超时则将引发 asyncio.TimeoutError。for f in as_completed(aws):
earliest_result = await f
asyncio 中协程多个 Task 的执行顺序是不可预测的,为了支持并发,asyncio 模块提供了一些低级的同步原子操作,这些同步原语和模块 threading
中的同步原语作用类似,需要注意的是,asyncio 中的同步原语不是线程安全的。
import asyncio
async def foo(m_lock, name):
async with m_lock:
loop = 1
while loop < 5:
print(name)
await asyncio.sleep(1)
loop += 1
async def main():
lock = asyncio.Lock()
await asyncio.gather(foo(lock, "A"), foo(lock, "B"),)
asyncio.run(main())
import asyncio
async def waiter(event):
print('waiting for it ...')
await event.wait()
print('... got it!')
async def main():
# Create an Event object.
event = asyncio.Event()
# Spawn a Task to wait until 'event' is set.
waiter_task = asyncio.create_task(waiter(event))
# Sleep for 1 second and set the event.
await asyncio.sleep(1)
event.set()
# Wait until the waiter task is finished.
await waiter_task
asyncio.run(main())
event
类似,区别是 Condition
可以选择通知的协程数量import asyncio
async def consumer(con, name):
async with con:
print(f"{name} is blocked")
await con.wait()
print(f"{name} is alive")
async def active_condition(con):
for i in range(1, 3):
async with con:
print(f"激活 {i} 个 task")
con.notify(i)
await asyncio.sleep(2)
async with con:
print("激活所有task")
con.notify_all()
async def main():
con = asyncio.Condition()
task_list = []
for i in range(1, 7):
task_list.append(asyncio.create_task(consumer(con, i)))
task_list.append(asyncio.create_task(active_condition(con)))
await asyncio.wait(task_list)
asyncio.run(main())
import asyncio
async def consumer(seq, name):
async with seq:
print(f"{name} 开始执行")
await asyncio.sleep(2)
print(f"{name} 执行完成")
async def main():
seq = asyncio.Semaphore(2) # 同时允许两个 task 执行
task_list = []
for i in range(1, 5):
task_list.append(asyncio.create_task(consumer(seq, i)))
await asyncio.wait(task_list)
asyncio.run(main())
# 输出
1 开始执行
2 开始执行
1 执行完成
2 执行完成
3 开始执行
4 开始执行
3 执行完成
4 执行完成
如果信号量的内部计数器设置为默认1 seq = asyncio.Semaphore(1)
,输出将会是下面的样子,一个一个的执行:
1 开始执行
1 执行完成
2 开始执行
2 执行完成
3 开始执行
3 执行完成
4 开始执行
4 执行完成
asyncio 队列被设计成与标准库 queue
模块类似。尽管 asyncio 队列不是线程安全的,但是他们是被设计专用于 async/await 代码。
asyncio 队列中有三种类型,详细介绍如下:
"""
Queue 使用示例
"""
import asyncio
async def consumer(name, queue):
while True:
obj = await queue.get() # get 没有元素后会永远阻塞在这里,还有一个非阻塞方法 get_nowait(),不过在队列为空是调用非阻塞方法会引发异常 QueueEmpty
print(f"{name} got the {obj}")
queue.task_done()
size = queue.qsize() # 可以获取队列的当前长度
async def producer(queue):
for i in range(1, 11):
queue.put_nowait(i) # 放置方法还有一个阻塞方法 put,在队列设置 maxsize 并且满后使用 put 会阻塞
await asyncio.sleep(2)
async def main():
queue = asyncio.Queue()
tasks = []
for i in range(1, 3):
task = asyncio.create_task(consumer(f'worker-{i}', queue))
tasks.append(task)
p_task = asyncio.create_task(producer(queue))
tasks.append(p_task)
await asyncio.gather(*tasks)
asyncio.run(main())
"""
LifoQueue 使用示例
"""
async def consumer(name, queue):
while True:
await asyncio.sleep(4)
obj = await queue.get()
print(f"{name} get value {obj}")
queue.task_done()
async def producer(queue):
for i in range(1, 11):
queue.put_nowait(i)
print(f"set value {i}")
await asyncio.sleep(1)
async def main():
queue = asyncio.LifoQueue()
tasks = []
for i in range(1, 3):
task = asyncio.create_task(consumer(f'worker-{i}', queue))
tasks.append(task)
p_task = asyncio.create_task(producer(queue))
tasks.append(p_task)
await asyncio.gather(*tasks)
asyncio.run(main())
# 输出
set value 1
set value 2
set value 3
set value 4
worker-1 get value 4
worker-2 get value 3
set value 5
set value 6
set value 7
set value 8
worker-1 get value 8
worker-2 get value 7
set value 9
set value 10
worker-1 get value 10
worker-2 get value 9
worker-1 get value 6
worker-2 get value 5
worker-1 get value 2
worker-2 get value 1
"""
PriorityQueue 优先队列使用
"""
import asyncio
import random
async def consumer(name, queue):
while True:
await asyncio.sleep(5)
p_num, obj = await queue.get() # get 到的值分两部分,第一个是优先级值,第二个是数据对象
print(f"{name} get value {p_num}, {obj}")
queue.task_done()
async def producer(queue):
for i in range(1, 6):
num = random.randint(1, 100)
queue.put_nowait((num, i,)) # put 的值是元祖形式,第一个值是优先级,第二个是数据对象
print(f"set value {num}")
await asyncio.sleep(1)
async def main():
queue = asyncio.PriorityQueue()
tasks = []
for i in range(1, 3):
task = asyncio.create_task(consumer(f'worker-{i}', queue))
tasks.append(task)
p_task = asyncio.create_task(producer(queue))
tasks.append(p_task)
await asyncio.gather(*tasks)
asyncio.run(main())
流是用于处理网络连接的高级 async/await-ready 原语,允许发送和接收数据。流 操作主要有以下几个函数组成:
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)
:建立网络连接并返回一对 (reader, writer) 对象,返回的 reader 和 writer 对象是 StreamReader 和 StreamWriter 类的实例。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)
:启动套接字服务。client_connected_cb
回调函数会在一个连接建立后被调用,同样返回一对 reader 和 writer 对象。asyncio.open_unix_connection(path=None, *, loop=None, limit=None, ssl=None, sock=None, server_hostname=None, ssl_handshake_timeout=None)
:作用和上面的 open_connections
一样,区别是只适用于 Unix 平台。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)
:作用和上面的 start_server
,区别是只适用于 Unix 平台。StreamReader 对象
用于从 IO 流中读取数据的IO对象,不推荐直接实例创建,而是从上面的方法的返回值中获取。
常用方法:
IncompleteReadError
异常。StreamWriter 对象
用于向 IO 连接中写入数据的对象,不推荐直接实例创建,而是从上面的方法的返回值中获取。
常用方法:
write(data):写入数据到流中。
writelines(data):写入一个序列的字节内容到流中。
close()
is_closing()
示例:
"""
服务端
"""
import asyncio
async def handle_echo(reader, writer):
data = await reader.read(100)
message = data.decode()
addr = writer.get_extra_info('peername')
print(f"Received {message!r} from {addr!r}")
print(f"Send: {message!r}")
writer.write(data)
await writer.drain()
print("Close the connection")
writer.close()
async def main():
server = await asyncio.start_server(
handle_echo, '127.0.0.1', 8888)
addr = server.sockets[0].getsockname()
print(f'Serving on {addr}')
async with server:
await server.serve_forever()
asyncio.run(main())
"""
客户端
"""
import asyncio
async def tcp_echo_client(message):
reader, writer = await asyncio.open_connection(
'127.0.0.1', 8888)
print(f'Send: {message!r}')
writer.write(message.encode())
data = await reader.read(100)
print(f'Received: {data.decode()!r}')
print('Close the connection')
writer.close()
asyncio.run(tcp_echo_client('Hello World!'))
对于上面的各种用法,要有一个认识基础,上面协程和 Task 的执行过程其实是单线程运行的,属于一种内部调度并发(需要理解并行与并发的区别),需要谨慎使用甚至不用全局变量和引用,这也是所有语言的并发编程中减少错误的最有效手段。
上面四小节是 asyncio 高级API,是3.7版本新加入的特性,相对应的对于3.7版本之前,还有一组低层次 API,来实现相关功能,官方已经建议开发者尽量使用高层次 API,因此低层次 API这里不在展开。另外需要说明的是,网上大部分文章对于 asyncio 模块的介绍都是基于低层次 API 的,参考前需要注意。
————————
继续阅读请点击:Python 进阶学习笔记之六:多线程编程
参考文章:进程、线程、异步 IO