使用'with'语句的非阻塞锁

19
据我所知,如果另一个线程已经获取了锁,下面的代码将被阻塞。似乎可以通过 lock.acquire(0) 实现非阻塞,但是我必须使用 try-finally 块代替 with 块。
lock = threading.Lock()

def func():
 with lock:
  # do something...

有没有一种方法可以实现非阻塞的锁获取?

5个回答

13
@contextmanager
def nonblocking(lock):
    locked = lock.acquire(False)
    try:
        yield locked
    finally:
        if locked:
            lock.release()

lock = threading.Lock()
with nonblocking(lock) as locked:
    if locked:
        do_stuff()

2
欢迎来到 Stack Overflow!虽然这段代码可能回答了问题,但提供关于为什么和/或如何回答问题的额外上下文可以提高其长期价值。我们不鼓励只有代码的答案。 - Ajean

10

有没有实现非阻塞锁获取的方法?

有。如果无法立即获取锁,只需引发异常。例如:

@contextlib.contextmanager
def non_blocking_lock(lock=threading.Lock()):
    if not lock.acquire(blocking=False):
        raise WouldBlockError
    try:
        yield lock
    finally:
        lock.release()

使用方法:

with non_blocking_lock():
    # run with the lock acquired

1
谢谢回答。无论如何,我必须使用lock.acquire函数。 - JaeJun LEE
@max:这不是 Python 中默认参数的工作方式。了解“最小惊奇”和可变默认参数可能会有所帮助。参考链接 - jfs
1
@J.F.Sebastian 啊,对了!我知道可变默认参数是如何工作的,但我没有意识到这个特性可以用于好事而不是坏事 :) 很酷,所以你可以通过上下文管理器定义获取一个有效的全局锁实例,并且你可以隐式地依赖它,只需使用此上下文管理器而无需参数。话虽如此,这似乎有点危险,因为意外省略参数会导致非常微妙的错误,这将非常难以检测。 - max
1
@max 如果你想要自己的锁,就传递你自己的锁。否则,你不应该向上下文管理器传递任何参数。是的,在多线程代码中很容易引入错误。 - jfs
1
@bavaza: false. (raise在try/finally之外) - jfs
显示剩余2条评论

1
如果你需要一个上下文管理器以非阻塞方式获取锁,但仍然重试,直到最终获取到锁,可以像这样操作:
@contextlib.contextmanager
def non_blocking_lock(lock : threading.Lock):
    # Waits as long as the lock can not be acquired, but releases the GIL in the meanwhile
    while not lock.acquire(blocking=False):
        pass

    try:
        yield   # Lock has been successfully acquired
    finally:
        lock.release()

它可以像普通的锁上下文管理器一样使用:

class TestClass:
    def __init__(self):
         self._lock = threading.Lock()
    
    def method(self):
         with non_blocking_lock(self._lock):
         # do something that should be only done from one thread at once

...与其他解决方案的不同之处在于,该锁是非阻塞的,直到释放锁才会保留GIL。我用它来解决了一些死锁问题。

与其他解决方案的区别在于,代码最终会被执行,上下文管理器在无法获取锁时不仅仅返回false或抛出异常。

如果您发现此解决方案存在任何问题,请纠正我。


0
锁的整个意义在于确保程序的某些部分只能由一个线程或进程执行。这是通过阻止任何试图获取锁的线程/进程来实现的,而其他东西则持有它。
如果您不希望获取锁会被阻塞,那么为什么首先要使用锁呢?可能是因为您想在等待时做其他事情吗?
要尝试获取锁l而不阻塞,请调用l.acquire(blocking=False)。如果未获得锁,则立即返回False。如果已经获得了锁,则返回True,并且您将继续持有该锁,直到调用其release()方法。
然而,这种形式在with语句中并不特别有用。通常,您希望受控代码(在with之后缩进的套件)仅在获取锁时运行,而不是查询是否已获取锁并采取两个替代操作。

谢谢。我必须使用lock.acquire()。 :D。 - JaeJun LEE
2
正是那些不寻常的情况需要像问题中所请求的解决方案。with结构允许清晰地处理那些锁获取成功的情况。 - quazgar

-1
如果您想在非阻塞锁中使用with语句,您可以首先检查它是否被锁定。如果是,则不进入with块。例如:
lock = threading.Lock()

def func():
    if not lock.locked():
        with lock:
            # do something...

2
没有任何东西可以防止这个被阻塞,因为在调用lock.locked()和使用with lock之间可能会发生上下文切换。 - bavaza

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接