Python异步编程:协程间通信

1、异步协程间通信方式类型

Python异步协程(coroutine)之间通信与多线程(multithreading)间通信相似, 标准库asyncio模块除了提供了asyncio.queue 用于协程间数据传递外, 还提供了几种同步通信对象。

同步原语说明
Lock同步锁,同时只允许 1个用户访问资源
Semaphore信号量,同时允许多个用户访问资源
Event信号事件机制,同时允许 1个用户访问资源
Condition类似于Lock与Event的组合
Barrier用于wait多个子线程并行运行

2、使用异步队列通信

主线程向协程,协程之间通信最好的方式是 queue, 但应使用asyncio.Queue模块,其支持async/await

1)Asyncio.Queue 主要方法与属性如下;

  • get()方法: 注意应该与await 一起使用, data = await queue.get(), 否则会报错。如果不在协程函数中,则直接用 data = 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]
  • 11
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值