Files
wechatauto-replica/wechatauto/utils/lock.py
T

128 lines
3.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""线程、进程与异步环境下的全局 UI 锁。"""
from __future__ import annotations
import asyncio
import functools
import inspect
import multiprocessing
import threading
from contextlib import asynccontextmanager, contextmanager
from typing import Any, Awaitable, Callable, TypeVar, overload
F = TypeVar("F", bound=Callable[..., Any])
AsyncReturn = TypeVar("AsyncReturn")
class LockManager:
"""提供跨线程/进程/异步的锁。
``process_lock``multiprocessing.Lock)不可重入:同一线程内嵌套
``acquire`` 会永久阻塞。因此用线程局部计数实现**同线程重入**——同一
线程重复获取时跳过进程锁(只需重入线程锁),保证
``@uilock`` 修饰的函数互相调用(如 ``Chat.ForwardVoiceMessage``
内部调用 ``VoiceMessage.forward_to``)不会死锁。
"""
process_lock = multiprocessing.Lock()
thread_lock = threading.RLock()
_async_lock: asyncio.Lock | None = None
_local = threading.local()
@classmethod
def _get_async_lock(cls) -> asyncio.Lock:
"""返回与当前事件循环绑定的 ``asyncio.Lock``。"""
loop = None
try:
loop = asyncio.get_running_loop()
except RuntimeError:
pass
lock = cls._async_lock
if lock is None or (loop and getattr(lock, "_loop", loop) is not loop):
lock = asyncio.Lock()
cls._async_lock = lock
return lock
@classmethod
@contextmanager
def acquire(cls):
"""同步环境下获取锁(同线程可重入)。"""
depth = getattr(cls._local, "depth", 0)
if depth > 0:
# 同线程嵌套:进程锁已被本线程持有,跳过它,只重入线程锁
with cls.thread_lock:
cls._local.depth = depth + 1
try:
yield
finally:
cls._local.depth = depth
return
with cls.process_lock:
with cls.thread_lock:
cls._local.depth = 1
try:
yield
finally:
cls._local.depth = 0
@classmethod
@asynccontextmanager
async def acquire_async(cls):
"""异步环境下获取锁(同线程可重入)。"""
depth = getattr(cls._local, "depth", 0)
if depth > 0:
async with cls._get_async_lock():
with cls.thread_lock:
cls._local.depth = depth + 1
try:
yield
finally:
cls._local.depth = depth
return
async with cls._get_async_lock():
with cls.process_lock:
with cls.thread_lock:
cls._local.depth = 1
try:
yield
finally:
cls._local.depth = 0
@overload
def uilock(func: Callable[..., Awaitable[AsyncReturn]]) -> Callable[..., Awaitable[AsyncReturn]]:
...
@overload
def uilock(func: F) -> F:
...
def uilock(func: F): # type: ignore[misc]
"""确保 UI 自动化操作串行执行的装饰器。"""
if inspect.iscoroutinefunction(func):
@functools.wraps(func)
async def async_wrapper(*args: Any, **kwargs: Any):
async with LockManager.acquire_async():
return await func(*args, **kwargs)
return async_wrapper
@functools.wraps(func)
def sync_wrapper(*args: Any, **kwargs: Any):
with LockManager.acquire():
return func(*args, **kwargs)
return sync_wrapper # type: ignore[return-value]
__all__ = ["LockManager", "uilock"]