1、异步协程间通信方式类型
协程(Coroutine) 是异步编程中的基本代码单元。多个协程通常并发执行以提高运行效率。经常遇到的情形,协程之间需要通信,如多个协程需要访问修改同1个数据,或者1个协程需要等待另1个协程,等等。 对于协程之间通信需求,标准库asyncio模块除了提供了异步队列(asyncio.queue) 用于协程间数据传递, 还提供了几种同步通信对象。
同步原语 | 说明 |
---|---|
Lock | 同步锁,同时只允许 1个用户访问资源 |
Semaphore | 信号量,同时允许多个用户访问资源 |
Event | 信号事件机制,同时允许 1个用户访问资源 |
Condition | 类似于Lock与Event的组合 |
Barrier | 用于wait多个子线程并行运行 |
2、使用异步队列通信
主线程向协程,协程之间通信最好的方式是 queue, 与标准库的Queue模块不同,asyncio.Queue模块支持 async/await 从队列存取数据。
1)Asyncio.Queue 主要方法与属性如下;
- get()方法: 注意应该与await 一起使用, data = await queue.get(), 否则会报错。如果不在协程函数中读数据,使用 get_nowait() 方法。
- put()方法,协程函数中,await queue.put(data), 非协程函数,用 queue.put_nowait(data)
- join()方法: 阻塞queue,直到队列中的所有元素都被取出并处理。
- 当1个元素被添加到队列后,unfinished tasks 总数增加
- 当1个消费者协程调用 task_done()时,表示其取出了1个元素,与该元素相关的工作已经完成,unfinished task 减少1。
- 当unfinished task降为零, join()将解除阻塞。
- 其它方法:
qsize(), full(), empty(),maxsize() 与标准库queue使用相同。
2)示例
import asyncio
import random
async def worker(name, queue):
while not queue.empty():
n =await queue.get()
print(f"worker get {n} from queue")
await asyncio.sleep(0.5)
queue.task_done() # 处理完1个元素,应发送task_done()
async def main():
# Create a queue that we will use to store our "workload".
qu = asyncio.Queue()
for _ in range(10):
qu.put_nowait(random.randrange(1,100,1))
task = asyncio.create_task(worker("worker",qu))
qu.join() # 阻塞队列,直到unfinished task 为0
# 生成异步任务集合
await asyncio.gather(task,return_exceptions=True)
print(qu.qsize())
if __name__ == "__main__":
asyncio.run(main())
3、同步通信
用于同步通信的对象包括: Lock, Event, Condition, Smartphore,Barrier, 与多线程使用方式基本相同。
1) Lock同步锁
用于控制多个协程访问同1个资源,避免同抢
通过示例了解其用法:
import asyncio
import random
counter = 0 # 全局变量
async def foo(num, lock):
await asyncio.sleep(1 + random.random())
await lock.acquire() # 获取锁
try:
global counter
print("coro foo is running, get ", num)
counter += 1
print("counter: ", counter)
finally:
lock.release() # 释放锁
return num*2
async def main():
print("main start")
lock = asyncio.Lock()
tasks = [foo(i, lock) for i in range(1, 6)]
res = await asyncio.gather(*tasks)
print("main end, res is ", res)
if __name__ == "__main__":
asyncio.run(main())
lock的使用,也可以用context with 语法
async with lock:
# work with shared resource
2) Semaphore 信号量通信
信号量也是用于同步,但允许同时有多个访问者。
其实现原理: 信号量维护一个内部计数器,该计数器会随每次acquire() 调用递减并随每次release() 调用递增。当acquire() 发现其值为零时,它将保持阻塞直到有某个任务调用了release()。
应用场景:
- 用于访问支持并发操作的资源 ,如mysql允许同时5个访问
编程主要步骤:
主线程中,创建semaphore 对象
sem = asyncio.Semaphore(10) #初始值为10,表示允许 10个协程同时访问资源。
子线程中主要用法
await sem.acquire()
try:
# work with shared resource
finally:
sem.release()
也可以用with语法代替
async with sem:
# work with shared resource
例: 100个协程访问共享资源,允许同时访问数量为10。
import time
import asyncio
import aiohttp
from bs4 import BeautifulSoup
async def get_title(semaphore, url):
async with semaphore:
print("正在采集:", url)
async with aiohttp.request('GET', url) as res:
html = await res.text()
soup = BeautifulSoup(html, 'html.parser')
title_tags = soup.find_all(attrs={'class': 'item-title'})
header_tags = soup.select(".event > .item-info > header")
info = []
for h in header_tags:
event = h.select("a")[0].text
time_info = h.select(".text-info")[0].text
info.append((event,time_info))
print(info)
async def main():
semaphore = asyncio.Semaphore(10) # 控制每次最多执行 10 个协程
# 创建100个协程,但同时最多执行10个
tasks = [asyncio.ensure_future(get_title(semaphore, "http://www.lishiju.net/hotevents/p{}".format(i))) for i in
range(100)]
dones, pendings = await asyncio.wait(tasks)
for task in dones:
print(len(task.result()))
if __name__ == '__main__':
start_time = time.perf_counter()
asyncio.run(main())
print("代码运行时间为:", time.perf_counter() - start_time)
3) Event 同步事件
作用: 向协程通知某事件的发生.
场景: 适用于通知多个协程采取动作,如通知所有协程优雅地退出,
编程步骤:
主线程: 创建Event全局对象
event = asyncio.Event() # 其后传给协程任务函数
协程任务函数:
if not event.is_set():
await event.wait()
# do something
示例:
mport asyncio
event = asyncio.Event()
async def periodic_signaller(interval):
while True:
await asyncio.sleep(interval)
event.set()
event.clear()
async def dependent_task(id):
while True:
await event.wait()
print(f'Task {id} performing operation.')
async def main():
tasks = [dependent_task(i) for i in range(5)]
signaller = periodic_signaller(5)
await asyncio.gather(signaller, *tasks)
asyncio.run(main())
4) Condition 条件同步
asyncio 条件同步可被任务用于等待某个事件发生,然后获取对共享资源的独占访问。
在本质上,Condition 对象合并了Event 和Lock 的功能。
应用场景:
合并了Event与Lock功能,用某一事件发生时, 通知协程对独享资源操作。 如收到保存日志指令,各协程将缓存中的日志写入同1个日志文件,用condition 可避免同时写文件。
编程步骤
主线程内: 创建condition 对象
cond = asyncio.Condition()
协程任务函数内:
async with cond:
await cond.wait()
with语句相当于
await cond.acquire()
try:
await cond.wait()
finally:
cond.release()
主要方法说明:
- coroutine acquire() 获取下层的锁。 notify(), 唤醒等待此条件的1个任务。
- notify_all() 唤醒所有正在等待此条件的任务。锁必须在此方法被调用前被获取并在随后被快速释放。
- release() 释放下层的锁。
- wait() 等待直至收到通知。这个方法会释放下层锁,保持阻塞直到被notify_all() 调用所唤醒。一旦被唤醒,Condition 会重新获取它的锁并且此方法将返回 True。
- wait_for( 条件函数 ) ,等待某个函数执行完成,条件函数返回值应该为boolean
示例 : 协程goo 必须等待foo的notify,才能访问work_list数据。
import asyncio
async def foo(condition, work_list):
work_list.append(99)
await asyncio.sleep(1)
print("foo sending notification....")
async with condition:
condition.notify()
async def goo(condition, work_list):
print(f"goo waiting for notification...")
async with condition:
await condition.wait()
print(f"goo got notification...")
work_list.append(100)
async def main():
condition = asyncio.Condition()
work_list = []
print("main waiting for data ....")
task1 = asyncio.create_task(foo(condition, work_list))
task2 = asyncio.create_task(goo(condition, work_list))
await asyncio.gather(task1, task2)
print(f"Got data {work_list}")
if __name__ == "__main__":
asyncio.run(main())
示例 2:
from random import random
import asyncio
async def task(condition, number):
print(f'Task {number} waiting...')
# acquire the condition
async with condition:
await condition.wait()
value = random()
await asyncio.sleep(value)
print(f'Task {number} got {value}')
# main coroutine
async def main():
condition = asyncio.Condition()
# create and start many tasks
tasks = [asyncio.create_task(task(condition, i)) for i in range(5)]
await asyncio.sleep(1)
async with condition:
condition.notify_all()
_ = await asyncio.wait(tasks)
# run the asyncio program
asyncio.run(main())
5) Barrier同步
asyncio.Barrier(屏障)通信,其用法与threading 多线程中的Barrier用法基本相同。
主要用途:
让多个子线程并行执行。
编程步骤:
- Step-1 主线程:定义1个Barrier对象 barrier = Asyncio.Barrier(5) , 5表示门限值
- Step-2 创建5个子线程,并将barrier对象做为参数传入子线程
- Step-3 在子线程中,运行barrier.wait() , 当5个子线程都运行了wait()后,barrier达到门限值,释放所有线程。
示例;
import asyncio
from random import randint
async def foo(barrier: asyncio.Barrier, id: int):
print(f"coroutine {id} waiting at the barrier.")
await asyncio.sleep(randint(1, 3))
await barrier.wait()
print(f"coroutine {id} has crossed the barrier.")
return id
async def main():
barrier = asyncio.Barrier(5)
tasks = [asyncio.create_task(foo(barrier, i)) for i in range(1, 6)]
results = await asyncio.gather(*tasks)
print(results)
asyncio.run(main())
output:
coroutine 1 waiting at the barrier.
coroutine 2 waiting at the barrier.
coroutine 3 waiting at the barrier.
coroutine 4 waiting at the barrier.
coroutine 5 waiting at the barrier.
coroutine 3 has crossed the barrier.
coroutine 4 has crossed the barrier.
coroutine 1 has crossed the barrier.
coroutine 5 has crossed the barrier.
coroutine 2 has crossed the barrier.
[1, 2, 3, 4, 5]