【问题标题】:Python Twisted's DeferredLockPython Twisted 的 DeferredLock
【发布时间】:2013-09-25 19:54:12
【问题描述】:

谁能提供一个例子并解释何时以及如何使用 Twisted 的DeferredLock。

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

【问题讨论】:

  • 很好,甚至不知道DeferredLock 存在。这对我来说可能会派上用场,但我最终不得不自己实现它,尽管使用了不同的范例..

标签: python twisted


【解决方案1】:

当您有一个异步的临界区并且需要防止重叠(有人可能会说“并发”)执行时,请使用 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 客户端 API http.GET 正在返回一个在 HTTP 请求完成时触发的 Deferred。如果你还不熟悉它们,你现在应该去阅读它们。

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

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

当然,你可以通过不共享状态来更轻松地实现并发使用安全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, 在单个 NetworkCounter 实例上对 next 的所有调用之间共享,但这不会导致并发使用时的任何实现问题。在诸如主要与 Twisted 一起使用的协作多任务系统中,在功能或操作的中间从来没有上下文切换。在self._count += 1 和result = self._count 行之间不能有从一个操作到另一个操作的上下文切换。它们将始终以原子方式执行,您不需要在它们周围加锁以避免重入或并发导致的损坏。

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

【讨论】:

  • 一如既往,感谢 Jean-Paul 的精彩解释!
  • 在上一个示例中仍然存在并发的不安全行为是否正确,即 self._count += 1 在 result = self._count 之前可能发生了两次?
  • 好的,我想我自己搞定了。异步 != 并行
猜你喜欢
  • 1970-01-01
  • 2013-01-19
  • 2011-06-11
  • 2012-10-29
  • 1970-01-01
  • 1970-01-01
  • 2011-02-12
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多