【问题标题】:Does Python's asyncio lock.acquire maintain order?Python 的 asyncio lock.acquire 是否维持秩序?
【发布时间】:2019-05-03 00:34:55
【问题描述】:

如果我有两个功能在做

async with mylock.acquire():
    ....

一旦锁被释放,是保证先等待的就赢,还是选择的顺序不同? (例如随机、任意、最新等)

我问的原因是,如果它不是先来先服务的,那么很容易出现饥饿的情况,即第一个尝试获取锁的函数永远不会赢得它。

【问题讨论】:

标签: python synchronization python-asyncio


【解决方案1】:

当我们谈论某事物的工作原理时,区分规范中表达的保证和实现的副作用很重要。第一个不应该更改(至少在主要版本中),第二个可以在将来的任何时间更改。

Martijn 的回答清楚地表明当前的实现保留了顺序。未来的保障呢?

官方文档for Python 3.6提供保证:

当 release() 调用将状态重置为解锁时,只有一个协程继续; 正在处理在 acquire() 中阻塞的第一个协程

有趣的是文档for Python 3.7 和文档for Python 3.8 dev 都没有这一行,但不确定它是否是故意的。但是github上的类的文档字符串has保证。

还值得一提的是threading.Lock(asyncio 锁的原型)明确表示订单未定义:

当 release() 调用将状态重置为解锁时,只有一个线程继续; 未定义哪个等待线程继续进行,并且可能因实现而异。


长话短说,现在只有类的文档字符串承诺维持秩序。还需要注意的是,锁定的实现不太可能在最近的将来发生更改。

然而想象一下,有人会改变它(例如,为了提高性能)。 docstring 是否足以防止以未定义的顺序实现锁定?由您决定。

如果您的代码严重依赖于保持顺序,并且如果您创建自己的锁(子)类来明确保证顺序(OrderedLock 或其他东西),那么它的生命周期很长。你可以只卖掉当前的实现。

如果情况更简单,您可以选择不理会它并使用当前的实现。

【讨论】:

  • 它看起来像一个文档错误,尤其是在 asyncio 文档最近经历了大修的情况下。
  • 警告区分记录的行为和当前实现的意外,在这种情况下它可能不适用。首先,实现不遗余力地创建一个deque 来保证顺序——至少是一个意图的暗示。其次,保证对于异步 IO 用例很有意义,并且在其他可比较的环境(JavaScript 回调、Tokio 线程池)中也提供了类似的保证。假设的替代实现将有非常充分的理由保留当前行为。最后,文档字符串仍然包含保证。
  • @user4815162342 同意您的所有观点。然而,安全总比后悔好,不是吗?如果项目很重要并且长寿和解锁顺序很重要,我更愿意强调我的Lock 不仅锁定,而且还按照OrderedLock 之类的类名顺序解锁(供应商实施将花费最少的精力)。当然,这更多的是个人喜好而不是确定性。
  • 在这种特殊情况下,我认为最好的方法是提交文档错误,即要求 asyncio 开发人员澄清。
  • 谢谢,我现在创建了一个拉取请求。
【解决方案2】:

是的,等待锁的任务被添加到队列中,并以 FIFO 为基础唤醒。

具体来说,当尝试获取锁定的锁时,会创建一个 future 来等待锁可用的信号,称为 waiter。这个服务员被添加到collections.deque()双端队列,created in Lock.__init__()

self._waiters = collections.deque()

当锁被当前持有它的任务释放时,Lock._wake_up_first() method 被调用:

def _wake_up_first(self):
    """Wake up the first waiter if it isn't done."""
    try:
        fut = next(iter(self._waiters))
    except StopIteration:
        return


    # .done() necessarily means that a waiter will wake up later on and
    # either take the lock, or, if it was cancelled and lock wasn't
    # taken already, will hit this again and wake up a new waiter.
    if not fut.done():
        fut.set_result(True)

Future.set_result() call 标志着未来已经完成。这究竟是如何导致等待未来重新获得控制权的任务取决于实现,但通常这是通过提供给事件循环的回调函数来完成的,以便尽早调用。

Lock.acquire() method 负责添加和删除期货(因为这是在发出信号后将返回的未来):

fut = self._loop.create_future()
self._waiters.append(fut)

# Finally block should be called before the CancelledError
# handling as we don't want CancelledError to call
# _wake_up_first() and attempt to wake up itself.
try:
    try:
        await fut
    finally:
        self._waiters.remove(fut)
except futures.CancelledError:
    if not self._locked:
        self._wake_up_first()
    raise

所以如果锁被锁定,当前任务通过创建一个future对象来等待,该对象被添加到_waiters队列中,等待future。这会阻止任务,直到未来有结果(await fut 直到那时才会返回)。事件循环不会给这个任务任何处理时间。

另一个当前持有锁并释放它的任务将导致来自_waiters 队列的第一个(最长等待)future 有一个结果集,间接导致正在等待该future 的任务再次变为活动状态。当锁释放任务将控制权交还给事件循环(等待其他事情时),事件循环将控制权交给等待该未来的任务,未来返回到await fut行,未来从队列和锁被给予在那个未来等待的任务。

这里有一个Lock.acquire() 方法显式处理的竞争条件情况:

  1. 任务 A 释放锁,队列持有等待锁的任务 B 的未来。未来已成定局。
  2. 事件循环将控制权交给第三个任务 C,该任务 C 正在等待不相关但现在再次处于活动状态,并且此任务运行尝试获取锁的代码。

任务 C 不会给予锁,但是,因为在 Lock.acquire() 方法的顶部是这个测试:

if not self._locked and all(w.cancelled() for w in self._waiters):
    self._locked = True
    return True

not self._locked 在他的情况下是正确的,因为任务 A 已经发布了它。但all(w.cancelled() for w in self._waiters) 不是,因为任务 B 在队列中有一个活动的、未取消的未来。因此,任务 C 将自己的服务员未来添加到队列中。 _waiters 队列中具有活动期货的未锁定锁实际上被视为已锁定。

【讨论】:

  • 你写的东西是规范保证的还是当前实现的细节?
  • @MikhailGerasimov 文档不保证;不,但asyncio.locks 原语的意图是尽可能“理智地”工作。所有原语都使用这种有序的方法。
  • Semaphore 实际上没有。它有底层的deque 建议它这样做,但它的acquire() 不会检查其中的现有服务员,因此在release() 之后有一个竞争条件,新呼叫者可以在“合法”服务员之前获取许可证那是预定醒来的。还有一个不好的副作用,因为许可证已经被拿走了,现有的醒着的服务员把自己放回队伍后面的deque,进一步搞砸了订单。
猜你喜欢
  • 2014-07-14
  • 2014-11-05
  • 2017-12-23
  • 1970-01-01
  • 2017-10-19
  • 2018-11-08
  • 2017-03-04
  • 1970-01-01
  • 2015-10-28
相关资源
最近更新 更多