Python的asyncio lock.acquire是否维持顺序?

时间:2019-05-02 11:15:24

标签: python synchronization python-asyncio

如果我有两个功能在做

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

一旦释放了锁,是否可以保证等待的第一个赢,还是选择的顺序不同? (例如,随机,任意,最新等)

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

2 个答案:

答案 0 :(得分:2)

当我们谈论某种事物如何工作时,重要的是要区分规范中表示的保证和实现的副作用。第一个不应更改(至少在主要版本内),第二个可以在将来随时更改。

Martijn的答案清楚地表明,当前的实现可以保持秩序。那么对未来的保证呢?

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

  

当release()调用将状态重置为解锁时,只有一个协程进行。 在acquire()中被阻止的第一个协程正在处理

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

还值得一提的是,threading.Lock(异步锁的原型)明确表示未定义顺序:

  

当release()调用将状态重置为解锁时,只有一个线程继续运行; 哪个等待线程继续进行未定义,并且在不同的实现中可能会有所不同。


长话短说,目前只有班级的文档字符串保证维护秩序。还要公平地指出,锁的实现不太可能在不久的将来更改。

但是,请想象有人会对其进行更改(例如,以提高性能)。 docstring是否足以防止以未定义的顺序实现锁定?由您决定。

如果您的代码严重依赖于保留顺序并预期具有较长的生命周期,那么创建自己的锁(子)类将明确保证顺序(OrderedLock或其他)就可以了。您可以将当前的实施方式卖掉。

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

答案 1 :(得分:1)

是的,正在等待锁的任务被添加到队列中,并以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

因此,如果锁已锁定,则通过创建一个将来对象(使其添加到_waiters队列中)来使当前任务等待,并等待将来。这将阻塞任务,直到将来有结果为止(await fut直到那时都不会返回)。事件循环不会给此任务任何处理时间。

当前持有该锁并释放该锁的另一个任务将导致_waiters队列中的第一个(等待时间最长)的未来具有结果集,从而间接导致正在等待该未来的任务再次变为活动状态。当释放锁的任务将控制权移交给事件循环(等待其他事件)时,事件循环将控制权移交给等待该未来的任务,未来将返回到await fut行,未来将被删除。从队列中获得,然后将锁授予在该将来等待的任务。

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

  1. 任务A释放锁,队列中有任务B等待锁的未来。未来已定。
  2. 事件循环将控制权交给了第三个任务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队列中具有活跃期货的解锁锁实际上被认为是锁定的。