Personal Blog

develop

异步协程中的Lock

介绍python3中异步协程中的Lock的使用方法

1. asyncio.Lock是什么?

在 Python 的asyncio模块中,Lock是一种用于异步环境的同步原语,主要用于防止多个协程同时访问共享资源,确保临界区操作的原子性。

  1. 等待队列:存储因获取不到锁而被暂停的协程,遵循 FIFO 公平性。
    • 等待队列严格按照协程调用acquire()的顺序排序,释放锁时总是唤醒最早进入队列的协程,避免某个协程长期饥饿。
  2. Lock:互斥锁,是是允许协程获得许可访问的凭证,保护对竞争资源的并发访问,避免多协程的竞态条件形成。

核心概念

asyncio.Lock的内部实现依赖于事件循环的调度机制,核心是维护两个关键状态:锁的当前状态和等待队列。这是协程中最基本、最常用的同步原语。

  • 锁用于保护共享资源的访问(确保状态修改的原子性)。
  • 等待队列用于存放因未获得许可访问的凭证而暂停的协程。

主要方法

  1. acquire():当协程调用await lock.acquire()时,首先检查_locked状态
    • 若_locked为False(未锁定):直接将_locked设为True,返回True,协程继续执行。
    • 若_locked为True(已锁定):创建一个Future对象,将其加入_waiters队列,然后暂停当前协程(通过await future),并将控制权交还给事件循环。
  2. 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. 结果说明

由结果可知:

  1. 主协程开始后,初始化Lock对象
  2. 创建10个协程,每个协程原理上都可以同时并发运行,并且同时访问共享变量counter并加1
  3. 这里因为有锁,并且每个任务的处理周期要全程获取锁,然后修改变量的状态,所以任务之间最终变成了串行执行,而不是并发执行

所以,Lock的作用就是牺牲并发度,但是能确保数据的一致性。

DEVELOP · ASYNCIO
develop python3 async coroutine