简体   繁体   English

Python的asyncio lock.acquire是否维护秩序?

[英]Does Python's asyncio lock.acquire maintain order?

If I have two functions doing 如果我有两个功能在做

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

Once the lock is released, is it guaranteed that the first to await will win, or is the order selected differently? 锁定被释放后,是否可以保证第一个等待获胜,或者订单选择不同? (eg randomly, arbitrarily, latest, etc.) (例如随机,任意,最新等)

The reason I'm asking, if it is not first-come-first-served, there might easily be a case of starvation where the first function attempting to acquire the lock never gets wins it. 我问的原因是,如果它不是先到先得的,可能很容易出现饥饿情况,试图获得锁定的第一个函数永远不会获胜。

When we talk about how something works it's important to distinguish guarantee expressed in specification and side-effect of implementation. 当我们谈论某些事情如何运作时,区分规范中表达的保证和实施的副作用是很重要的。 First one shouldn't be changed (at least, within major version), second one can be changed any time in the future. 第一个不应该更改(至少在主要版本内),第二个可以在将来的任何时候更改。

Martijn's answer clearly shows that current implementation preserves order. Martijn的回答清楚地表明当前的实施保留了秩序。 What about guarantee for future? 如何保证未来?

Official documentation for Python 3.6 provides guarantee: Python 3.6的官方文档提供了保证:

only one coroutine proceeds when a release() call resets the state to unlocked; 当release()调用将状态重置为未锁定时,只有一个协同程序进行; first coroutine which is blocked in acquire() is being processed . 正在处理在acquire()中被阻止的第一个协同程序

Interesting thing is that neither documentation for Python 3.7 nor documentation for Python 3.8 dev have this line, not sure if it's intentional though. 有趣的是, Python 3.7的文档和Python 3.8 dev的文档都没有这一行,但不确定它是否是有意的。 However class's docstring on github has guarantee. 但是github 上的类的docstring 保证。

It's also worth mentioning that threading.Lock (prototype for asyncio's lock) explicitly says that order is undefined: 还值得一提的是, threading.Lock (asyncio锁的原型)明确表示订单未定义:

only one thread proceeds when a release() call resets the state to unlocked; 当release()调用将状态重置为未锁定时,只有一个线程继续进行; which one of the waiting threads proceeds is not defined , and may vary across implementations. 等待线程中的哪一个进行的未定义 ,并且可能因实现而异。


Long story short, right now only class's docstring promises to maintain order. 长话短说,现在只有班级的docstring承诺维持秩序。 It's also fair to note that implementation of lock is unlikely to being changed in the nearest future. 值得注意的是,锁定的实施在最近的将来不太可能发生变化。

Yet imagine however someone will change it (to increase performance, for example). 然而,想象一下,有人会改变它(例如,为了提高性能)。 Will docstring be enough to prevent from implementing lock with undefined order? docstring是否足以阻止使用未定义的顺序实现锁定? It's up to you to decide. 由你来决定。

If your code critically depends on preserving order and expected to have long life cycle nothing bad if you create your own lock (sub)class which will explicitly guarantee order ( OrderedLock or something). 如果您的代码严格依赖于保留顺序并且期望具有较长的生命周期,那么如果您创建自己的锁(子)类将明确保证顺序( OrderedLock或其他),则没有什么不好。 You may just vendorize current implementation. 您可能只是供应当前的实现。

If situation is simpler you may choose not to bother with it and use current implementation. 如果情况更简单,您可以选择不打扰它并使用当前实现。

Yes, tasks that are waiting on the lock are added to a queue, and woken on a FIFO basis. 是的,等待锁的任务被添加到队列中,并以FIFO为基础唤醒。

Specifically, when attempting to acquire a locked lock, a future is created that waits for a signal that the lock has become available, called a waiter . 具体地说,当试图获取锁定锁时,将创建一个等待锁已变得可用的信号的未来 ,称为服务员 This waiter is added to a collections.deque() double-ended queue, created in Lock.__init__() 此服务器被添加到collections.deque()双端队列, Lock.__init__()

self._waiters = collections.deque()

When the lock is released by the task currently holding it, the Lock._wake_up_first() method is called: 当当前持有锁的任务释放锁时,将Lock._wake_up_first()方法

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)

The Future.set_result() call marks the future as done. Future.set_result()调用将未来标记为已完成。 How exactly this leads to the task awaiting on the future to regain control is implementation dependent, but usually this is done via a callback function given to the event loop to call at its earliest convenience. 究竟是如何导致等待未来重新获得控制的任务依赖于实现,但通常这是通过给予事件循环的回调函数来完成的,以便在最早的方便时调用。

The Lock.acquire() method is responsible for both adding and removing futures (as that's where the future will return to when signalled a result has been set): Lock.acquire()方法负责添加和删除期货(因为在设置结果的信号时,未来将返回到的地方):

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

So if the lock is locked, the current task is made to wait by creating a future object, which is added to the _waiters queue, and the future is awaited on. 因此,如果锁被锁定,则当前任务通过创建将来的对象(等待添加到_waiters队列)来等待,等待将来等待。 This blocks the task until the future has a result ( await fut won't return until then). 这会阻止任务直到将来有结果( await fut将在此之前不再返回)。 The event loop will not give this task any processing time. 事件循环不会为此任务提供任何处理时间。

Another task that currently holds the lock and releases it will cause the first (longest waiting) future from the _waiters queue to have a result set, indirectly causing the task that is waiting for that future to become active again. 当前持有锁并释放它的另一个任务将导致_waiters队列中的第一个(等待时间最长)未来具有结果集,间接导致等待该未来的任务再次变为活动状态。 When the lock-releasing task hands back control to the event loop (when awaiting on something else), the event loop hands control to the task waiting for that future, the future returns to the await fut line, the future is removed from the queue and the lock is given to the task that waited on that future. 当锁定释放任务将控制权移交给事件循环时(等待其他事情时),事件循环将控制权交给等待该未来的任务,将来返回到await fut线,未来将从队列中移除锁定给了等待那个未来的任务。

There is one race condition case here that the Lock.acquire() method explicitly handles: 这里有一个竞争条件, Lock.acquire()方法显式处理:

  1. Task A releases the lock, the queue holds a future for task B waiting for the lock. 任务A释放锁,队列为任务B等待锁的未来。 The future is set to done. 未来将完成。
  2. The event loop gives control to a third task C that was awaiting on something unreleated but is now active again, and this task runs code that tries to acquire the lock. 事件循环控制第三个任务C正在等待未完成但尚未激活的任务,此任务运行的代码尝试获取锁。

Task C won't be given the lock, however, because at the top of the Lock.acquire() method is this test: 但是,任务C不会被赋予锁定,因为在Lock.acquire()方法的顶部是这个测试:

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

not self._locked is true in his case, as task A has released it. not self._locked在他的情况下是正确的,因为任务A已经释放它。 But all(w.cancelled() for w in self._waiters) is not, as task B has an active, non-cancelled future in the queue. 但是all(w.cancelled() for w in self._waiters)是,因为任务B在队列中具有活动的,未取消的未来。 So task C is made to add their own waiter future to the queue. 因此,任务C将自己的服务员未来添加到队列中。 An unlocked lock with active futures in the _waiters queue is actually considered locked. _waiters队列中具有活动期货的解锁锁实际上被视为锁定。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM