10

有人可以提供一个示例并解释何时以及如何使用 Twisted 的DeferredLock

我有一个 DeferredQueue 并且我想我有一个我想阻止的竞争条件,但我不确定如何将两者结合起来。

4

1 回答 1

19

当您有一个DeferredLock异步的关键部分并且需要防止重叠(有人可能会说“并发”)执行时使用。

以下是此类异步临界区的示例:

class NetworkCounter(object):
    def __init__(self):
        self._count = 0

    def next(self):
        self._count += 1
        recording = self._record(self._count)
        def recorded(ignored):
            return self._count
        recording.addCallback(recorded)
        return recording

    def _record(self, value):
        return http.GET(
            b"http://example.com/record-count?value=%d" % (value,))

看看该next方法的两个并发使用将如何产生“损坏”结果:

from __future__ import print_function

counter = NetworkCounter()
d1 = counter.next()
d2 = counter.next()

d1.addCallback(print, "d1")
d2.addCallback(print, "d2")

给出结果:

2 d1
2 d2

这是因为在NetworkCounter.next对该方法的第一次调用完成使用该_count属性生成其结果之前,第二次调用开始。这两个操作共享一个属性并因此产生不正确的输出。

使用DeferredLock实例将通过阻止第二个操作开始直到第一个操作完成来解决此问题。你可以像这样使用它:

class NetworkCounter(object):
    def __init__(self):
        self._count = 0
        self._lock = DeferredLock()

    def next(self):
        return self._lock.run(self._next)

    def _next(self):
        self._count += 1
        recording = self._record(self._count)
        def recorded(ignored):
            return self._count
        recording.addCallback(recorded)
        return recording

    def _record(self, value):
        return http.GET(
            b"http://example.com/record-count?value=%d" % (value,))

首先,请注意NetworkCounter实例创建了自己的DeferredLock实例。的每个实例DeferredLock都是不同的,并且独立于任何其他实例运行。任何参与使用临界区的代码都需要使用相同的DeferredLock实例才能保护该临界区。如果两个NetworkCounter实例以某种方式共享状态,那么它们还需要共享一个DeferredLock实例——而不是创建自己的私有实例。

接下来,查看如何DeferredLock.run使用新_next方法调用(所有应用程序逻辑都已移入其中)。 NetworkCounter(也不是使用 的应用程序代码NetworkCounter)不调用包含临界区的方法。 DeferredLock被赋予了这样做的责任。这就是如何DeferredLock防止关键部分在“同一”时间被多个操作运行。在内部,DeferredLock将跟踪操作是否已开始但尚未完成。Deferred如果操作的完成被表示为虽然,它只能跟踪操作的完成。如果您熟悉Deferreds,您可能已经猜到此示例中的(假设的)HTTP 客户端 APIhttp.GET正在返回一个Deferred当 HTTP 请求完成时触发。如果你还不熟悉它们,你现在应该去阅读它们。

一旦Deferred表示操作结果的 触发 - 换句话说,一旦操作完成,DeferredLock将认为关键部分“不再使用”并允许另一个操作开始执行它。它将通过检查在临界区正在使用时是否有任何代码尝试进入临界区来做到这一点,如果是,它将运行该操作的函数。

第三,注意为了序列化对临界区的访问,DeferredLock.run必须返回一个Deferred. 如果临界区正在使用并被DeferredLock.run调用,则它不能启动另一个操作。因此,相反,它创建并返回一个新的Deferred. 当临界区不再使用时,下一个操作可以开始,当该操作完成时,调用Deferred返回的DeferredLock.run将得到它的结果。这一切最终对于任何已经期待 a 的用户来说都是相当透明的Deferred- 这只是意味着该操作似乎需要更长的时间才能完成(尽管事实是它可能需要相同的时间才能完成,但它等待a在它开始之前- 挂钟的效果是一样的)。

当然,你可以NetworkCounter通过不共享状态来更容易地实现并发使用安全:

class NetworkCounter(object):
    def __init__(self):
        self._count = 0

    def next(self):
        self._count += 1
        result = self._count
        recording = self._record(self._count)
        def recorded(ignored):
            return result
        recording.addCallback(recorded)
        return recording

    def _record(self, value):
        return http.GET(
            b"http://example.com/record-count?value=%d" % (value,))

此版本将用于NetworkCounter.next为调用者生成有意义结果的状态从实例字典(即,它不再是NetworkCounter实例的属性)移到调用堆栈(即,它现在是与实现方法调用的实际框架)。由于每次调用都会创建一个新框架和一个新闭包,因此并发调用现在是独立的,不需要任何类型的锁定。

最后,请注意,即使这个修改后的NetworkCounter.next仍然使用版本在单个实例上的所有调用中self._count共享,但在同时使用时不会对实现造成任何问题。在诸如主要与 Twisted 一起使用的协作式多任务系统中,在功能或操作的中间从来没有上下文切换。在和行之间不能有从一个操作到另一个操作的上下文切换。它们将始终以原子方式执行,您不需要在它们周围加锁以避免重入或并发导致的损坏。nextNetworkCounterself._count += 1result = self._count

最后两点——通过避免共享状态和函数内部代码的原子性来避免并发错误——结合起来意味着这通常DeferredLock不是特别有用。作为单个数据点,在我当前的工作项目(大量基于 Twisted)的大约 75 KLOC 中,没有使用.DeferredLock

于 2013-09-24T22:34:31.387 回答