develop
异步协程中的Lock
Table of Contents
1. asyncio.Lock是什么?
在 Python 的asyncio模块中,Lock是一种用于异步环境的同步原语,主要用于防止多个协程同时访问共享资源,确保临界区操作的原子性。
- 等待队列:存储因获取不到锁而被暂停的协程,遵循 FIFO 公平性。
- 等待队列严格按照协程调用acquire()的顺序排序,释放锁时总是唤醒最早进入队列的协程,避免某个协程长期饥饿。
- Lock:互斥锁,是是允许协程获得许可访问的凭证,保护对竞争资源的并发访问,避免多协程的竞态条件形成。
核心概念
asyncio.Lock的内部实现依赖于事件循环的调度机制,核心是维护两个关键状态:锁的当前状态和等待队列。这是协程中最基本、最常用的同步原语。
- 锁用于保护共享资源的访问(确保状态修改的原子性)。
- 等待队列用于存放因未获得许可访问的凭证而暂停的协程。
主要方法
- acquire():当协程调用await lock.acquire()时,首先检查_locked状态
- 若_locked为False(未锁定):直接将_locked设为True,返回True,协程继续执行。
- 若_locked为True(已锁定):创建一个Future对象,将其加入_waiters队列,然后暂停当前协程(通过await future),并将控制权交还给事件循环。
- release():当持有锁的协程调用lock.release()时
- 首先检查_locked是否为True(确保当前协程持有锁),若不是则抛出RuntimeError。
- 将_locked设为False。
- 若_waiters队列非空,从队列头部取出第一个Future对象,通过set_result(True)标记其完成,唤醒对应的等待协程。被唤醒的协程会再次尝试获取锁(此时_locked已为False,会成功获取,并将_locked重新设为True)。
2. 举个栗子来说明
import asyncio
import logging
# 配置日志格式
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(threadName)s - %(levelname)s - %(message)s',
datefmt='%H:%M:%S'
)
# 全局计数器
counter = 0
async def worker(lock: asyncio.Lock, name: str):
global counter
async with lock: # 自动调用acquire(),退出时自动调用release()
counter += 1
logging.info(f"Worker {name} 获得锁,开始操作,计数器值: {counter}")
await asyncio.sleep(1) # 模拟耗时操作
logging.info(f"Worker {name} 释放锁")
async def main():
global counter
lock = asyncio.Lock()
tasks = [worker(lock, f"{i+1}") for i in range(10)]
await asyncio.gather(*tasks)
logging.info(f"所有任务完成,最终计数器值: {counter}")
if __name__ == "__main__":
asyncio.run(main())
Log输出如下:
16:13:07 - MainThread - INFO - Worker 1 获得锁,开始操作,计数器值: 1
16:13:08 - MainThread - INFO - Worker 1 释放锁
16:13:08 - MainThread - INFO - Worker 2 获得锁,开始操作,计数器值: 2
16:13:09 - MainThread - INFO - Worker 2 释放锁
16:13:09 - MainThread - INFO - Worker 3 获得锁,开始操作,计数器值: 3
16:13:10 - MainThread - INFO - Worker 3 释放锁
16:13:10 - MainThread - INFO - Worker 4 获得锁,开始操作,计数器值: 4
16:13:11 - MainThread - INFO - Worker 4 释放锁
16:13:11 - MainThread - INFO - Worker 5 获得锁,开始操作,计数器值: 5
16:13:12 - MainThread - INFO - Worker 5 释放锁
16:13:12 - MainThread - INFO - Worker 6 获得锁,开始操作,计数器值: 6
16:13:13 - MainThread - INFO - Worker 6 释放锁
16:13:13 - MainThread - INFO - Worker 7 获得锁,开始操作,计数器值: 7
16:13:14 - MainThread - INFO - Worker 7 释放锁
16:13:14 - MainThread - INFO - Worker 8 获得锁,开始操作,计数器值: 8
16:13:15 - MainThread - INFO - Worker 8 释放锁
16:13:15 - MainThread - INFO - Worker 9 获得锁,开始操作,计数器值: 9
16:13:16 - MainThread - INFO - Worker 9 释放锁
16:13:16 - MainThread - INFO - Worker 10 获得锁,开始操作,计数器值: 10
16:13:17 - MainThread - INFO - Worker 10 释放锁
16:13:17 - MainThread - INFO - 所有任务完成,最终计数器值: 10
3. 结果说明
由结果可知:
- 主协程开始后,初始化Lock对象
- 创建10个协程,每个协程原理上都可以同时并发运行,并且同时访问共享变量counter并加1
- 这里因为有锁,并且每个任务的处理周期要全程获取锁,然后修改变量的状态,所以任务之间最终变成了串行执行,而不是并发执行
所以,Lock的作用就是牺牲并发度,但是能确保数据的一致性。
DEVELOP · ASYNCIO
develop python3 async coroutine
