gengx
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
"""
|
||||
发送互斥锁
|
||||
==========
|
||||
双引擎并存(引擎 A 截图 RPA + 引擎 B 数据直读)时,企业微信窗口是唯一共享
|
||||
资源,任意时刻只允许一个发送者触碰它。本模块用 Windows 命名互斥量实现
|
||||
跨进程/跨线程的发送互斥:
|
||||
|
||||
- 拿到锁的引擎才有资格调用 WeChatBot.send_reply();
|
||||
- 拿不到的引擎本轮直接跳过,绝不阻塞等待(等待会导致两个引擎互相拖死);
|
||||
- 持锁进程/线程崩溃时,Windows 会自动释放互斥量,不会出现"锁泄漏"。
|
||||
|
||||
非 Windows 环境(CI、单元测试、macOS/Linux 开发机)回退到进程内
|
||||
threading.Lock,保证模块可导入、可测试。
|
||||
"""
|
||||
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
|
||||
# 全局唯一的互斥量名。跨进程共享同一命名空间,任意数量的引擎进程都抢同一把锁。
|
||||
_MUTEX_NAME = "Global\\WeChatBotSendLock"
|
||||
|
||||
# WaitForSingleObject 返回码
|
||||
_WAIT_OBJECT_0 = 0x00000000
|
||||
_WAIT_ABANDONED = 0x00000080 # 拿到了锁,但上一个持有者是崩溃退出的
|
||||
_WAIT_TIMEOUT = 0x00000102
|
||||
|
||||
# 回退锁(非 Windows):进程内互斥,行为与命名互斥量一致
|
||||
_FALLBACK_LOCK = threading.Lock()
|
||||
|
||||
_handle = None
|
||||
_init_done = False
|
||||
_owner = "" # 当前持有者标识(引擎名:线程名),用于排查与日志
|
||||
_owner_until = 0.0 # 持有者预计释放时间(wall clock),仅用于日志统计
|
||||
_holder_tid = None # 持有锁的线程 id;防止同一线程重入(嵌套发送)
|
||||
|
||||
|
||||
def _windows_mutex_available() -> bool:
|
||||
"""Windows 且能加载 kernel32 时返回 True。"""
|
||||
global _handle, _init_done
|
||||
if _init_done:
|
||||
return _handle is not None
|
||||
_init_done = True
|
||||
if os.name != "nt":
|
||||
return False
|
||||
try:
|
||||
import ctypes
|
||||
from ctypes import wintypes # noqa: F401
|
||||
|
||||
_handle = ctypes.windll.kernel32.CreateMutexW(None, False, _MUTEX_NAME)
|
||||
return bool(_handle)
|
||||
except Exception:
|
||||
_handle = None
|
||||
return False
|
||||
|
||||
|
||||
def try_acquire(owner: str = "", timeout: float = 0.0) -> bool:
|
||||
"""非阻塞(或短超时)抢占发送锁。
|
||||
|
||||
owner 用于日志排查,例如 "engine_b:listener"。timeout 默认 0(不等待),
|
||||
调用方应优先用 0——拿不到就下一轮再试,避免双引擎互相阻塞。
|
||||
|
||||
注意:Windows 命名互斥量对同一线程是可重入的(可重复获取),这不符合
|
||||
「任意时刻只有一个发送者」的语义(同一线程内也不允许嵌套发送)。因此
|
||||
本函数额外记录持有线程 id,同线程重入一律返回 False。
|
||||
"""
|
||||
global _owner, _owner_until, _holder_tid
|
||||
tid = threading.get_ident()
|
||||
if _holder_tid == tid:
|
||||
return False
|
||||
if _windows_mutex_available():
|
||||
try:
|
||||
import ctypes
|
||||
|
||||
wait_ms = int(max(0.0, timeout) * 1000)
|
||||
rc = ctypes.windll.kernel32.WaitForSingleObject(_handle, wait_ms)
|
||||
# WAIT_ABANDONED 同样代表"这把锁现在归你了",只是上一个持有者是崩溃
|
||||
# 退出的。把它当失败直接 return,互斥量就永远留在本线程名下、再也没人
|
||||
# 释放——从此整个进程一条消息都发不出去。
|
||||
if rc in (_WAIT_OBJECT_0, _WAIT_ABANDONED):
|
||||
_owner = str(owner or "")
|
||||
_owner_until = time.time() + max(0.0, timeout)
|
||||
_holder_tid = tid
|
||||
return True
|
||||
return False
|
||||
except Exception:
|
||||
return False
|
||||
# threading.Lock 不允许"非阻塞 + 超时"同时出现(ValueError)。默认的
|
||||
# timeout=0 正是这种组合,非 Windows 环境下每一次抢锁都会直接抛异常。
|
||||
if timeout > 0:
|
||||
acquired = _FALLBACK_LOCK.acquire(blocking=True, timeout=timeout)
|
||||
else:
|
||||
acquired = _FALLBACK_LOCK.acquire(blocking=False)
|
||||
if not acquired:
|
||||
return False
|
||||
_owner = str(owner or "")
|
||||
_owner_until = time.time() + max(0.0, timeout)
|
||||
_holder_tid = tid
|
||||
return True
|
||||
|
||||
|
||||
def release() -> None:
|
||||
"""释放发送锁。未持锁时调用是安全的(Windows 互斥量允许)。"""
|
||||
global _owner, _owner_until, _holder_tid
|
||||
if _holder_tid is not None and _holder_tid != threading.get_ident():
|
||||
# 非持有线程尝试释放:忽略,避免误释放他人的锁
|
||||
return
|
||||
_holder_tid = None
|
||||
if _windows_mutex_available():
|
||||
try:
|
||||
import ctypes
|
||||
|
||||
ctypes.windll.kernel32.ReleaseMutex(_handle)
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
_FALLBACK_LOCK.release()
|
||||
except RuntimeError:
|
||||
pass
|
||||
_owner = ""
|
||||
_owner_until = 0.0
|
||||
|
||||
|
||||
def is_held() -> bool:
|
||||
"""当前进程是否持有发送锁(用于诊断日志)。"""
|
||||
if _holder_tid is not None:
|
||||
return True
|
||||
if _windows_mutex_available():
|
||||
try:
|
||||
import ctypes
|
||||
|
||||
rc = ctypes.windll.kernel32.WaitForSingleObject(_handle, 0)
|
||||
if rc in (_WAIT_OBJECT_0, _WAIT_ABANDONED):
|
||||
# 抢到了说明之前没持有,立即释放还原状态。WAIT_ABANDONED 也已经
|
||||
# 把所有权交给了本线程,同样必须还回去,否则这次"诊断"会把锁吞掉。
|
||||
ctypes.windll.kernel32.ReleaseMutex(_handle)
|
||||
return False
|
||||
return rc == _WAIT_TIMEOUT # 被别人持有
|
||||
except Exception:
|
||||
return False
|
||||
return _FALLBACK_LOCK.locked()
|
||||
|
||||
|
||||
def holder() -> str:
|
||||
"""当前持有者标识(仅本进程视角;跨进程时返回空串)。"""
|
||||
return str(_owner or "")
|
||||
|
||||
|
||||
# ── 上下文管理器:with send_lock("engine_b"): ... ───────────────────────────
|
||||
class _SendLockCtx:
|
||||
def __init__(self, owner: str, timeout: float):
|
||||
self.owner = owner
|
||||
self.timeout = timeout
|
||||
self.acquired = False
|
||||
|
||||
def __enter__(self):
|
||||
self.acquired = try_acquire(self.owner, self.timeout)
|
||||
return self.acquired
|
||||
|
||||
def __exit__(self, exc_type, exc, tb):
|
||||
if self.acquired:
|
||||
release()
|
||||
self.acquired = False
|
||||
return False
|
||||
|
||||
|
||||
def lock(owner: str = "", timeout: float = 0.0) -> _SendLockCtx:
|
||||
"""用法:with send_lock.lock("engine_b"): do_send()"""
|
||||
return _SendLockCtx(owner, timeout)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 冒烟测试:两个线程抢同一把锁,各自打印持锁时段
|
||||
import random
|
||||
|
||||
def worker(name):
|
||||
for i in range(3):
|
||||
if try_acquire(name, 0):
|
||||
print(f"[{name}] 第{i}轮拿到锁(持有者={holder()})")
|
||||
time.sleep(random.uniform(0.02, 0.08))
|
||||
release()
|
||||
else:
|
||||
print(f"[{name}] 第{i}轮未抢到锁,跳过")
|
||||
time.sleep(0.01)
|
||||
|
||||
t1 = threading.Thread(target=worker, args=("engine_a",))
|
||||
t2 = threading.Thread(target=worker, args=("engine_b",))
|
||||
t1.start()
|
||||
t2.start()
|
||||
t1.join()
|
||||
t2.join()
|
||||
print("[OK] 发送互斥锁冒烟测试完成")
|
||||
Reference in New Issue
Block a user