50

协程和资源获取的结合似乎会产生一些意想不到的(或不直观的)后果。

基本问题是这样的事情是否有效:

def coroutine():
    with open(path, 'r') as fh:
        for line in fh:
            yield line

它确实如此。(你可以测试一下!)

更深层次的担忧是,with它应该是 的替代品finally,您可以确保在块的末尾释放资源。协程可以在块内暂停和恢复执行,那么冲突如何解决的呢?with

例如,如果您在协程尚未返回时打开一个在协程内部和外部都具有读/写功能的文件:

def coroutine():
    with open('test.txt', 'rw+') as fh:
        for line in fh:
            yield line

a = coroutine()
assert a.next() # Open the filehandle inside the coroutine first.
with open('test.txt', 'rw+') as fh: # Then open it outside.
    for line in fh:
        print 'Outside coroutine: %r' % repr(line)
assert a.next() # Can we still use it?

更新

在前面的示例中,我打算进行写锁定文件句柄争用,但由于大多数操作系统为每个进程分配文件句柄,因此不会出现争用。(感谢@Miles 指出这个例子并没有多大意义。)这是我修改后的例子,它显示了一个真正的死锁情况:

import threading

lock = threading.Lock()

def coroutine():
    with lock:
        yield 'spam'
        yield 'eggs'

generator = coroutine()
assert generator.next()
with lock: # Deadlock!
    print 'Outside the coroutine got the lock'
assert generator.next()
4

5 回答 5

24

我真的不明白你在问什么冲突,也不明白这个例子的问题:对同一个文件有两个共存的独立句柄很好。

我不知道我在回答您的问题时了解到的一件事是生成器上有一个新的 close() 方法:

close()在生成器内引发一个新GeneratorExit异常以终止迭代。在接收到这个异常时,生成器的代码必须是 raiseGeneratorExitStopIteration.

close()当生成器被垃圾收集时调用,因此这意味着生成器的代码在生成器被销毁之前有最后一次运行的机会。这最后的机会意味着try...finally生成器中的语句现在可以保证工作;该finally子句现在总是有机会运行。这似乎是一些语言琐事,但使用生成器try...finally实际上是实现withPEP 343 描述的语句所必需的。

http://docs.python.org/whatsnew/2.5.html#pep-342-new-generator-features

这样就可以处理with在生成器中使用语句但它在中间产生但从不返回的情况 - 上下文管理器的__exit__方法将在生成器被垃圾收集时调用。


编辑

关于文件句柄问题:我有时会忘记存在不类似于 POSIX 的平台。:)

就锁而言,我认为 Rafał Dowgird 说的“你只需要注意,生成器就像任何其他拥有资源的对象一样”是一针见血。我认为该with声明在这里并不那么重要,因为此函数存在相同的死锁问题:

def coroutine():
    lock.acquire()
    yield 'spam'
    yield 'eggs'
    lock.release()

generator = coroutine()
generator.next()
lock.acquire() # whoops!
于 2009-03-26T10:12:08.520 回答
9

我不认为有真正的冲突。您只需要知道生成器就像任何其他拥有资源的对象一样,因此创建者有责任确保它正确完成(并避免与对象所拥有的资源发生冲突/死锁)。我在这里看到的唯一(次要)问题是生成器没有实现上下文管理协议(至少从 Python 2.5 开始),所以你不能只是:

with coroutine() as cr:
  doSomething(cr)

但必须:

cr = coroutine()
try:
  doSomething(cr)
finally:
  cr.close()

垃圾收集器close()无论如何都会做,但依赖它来释放资源是不好的做法。

于 2009-03-26T10:44:15.073 回答
1

因为yield可以执行任意代码,所以我会非常警惕对 yield 语句持有锁。但是,您可以通过许多其他方式获得类似的效果,包括调用可能已被覆盖或以其他方式修改的方法或函数。

然而,生成器总是(几乎总是)“关闭”,或者通过显式close()调用,或者只是通过垃圾收集。关闭生成器会在生成器内引发GeneratorExit异常,因此会运行 finally 子句、语句清理等。您可以捕获异常,但必须抛出或退出函数(即抛出StopIteration异常),而不是 yield。在您编写的情况下,依靠垃圾收集器关闭生成器可能是一种不好的做法,因为这可能会比您想要的更晚发生,并且如果有人调用 sys._exit(),那么您的清理可能根本不会发生.

于 2009-03-26T10:44:54.543 回答
1

对于 TLDR,这样看:

with Context():
    yield 1
    pass  # explicitly do nothing *after* yield
# exit context after explicitly doing nothing

Context完成后pass结束(即什么都不做),完成pass后执行yield(即恢复执行)。因此,控制后的with结束恢复yield

TLDR:释放控制with时,上下文保持不变。yield


实际上,这里只有两个相关的规则:

  1. 什么时候with释放它的资源?

    它在其块完成后立即执行一次。前者意味着它不会在a期间释放,因为这可能会发生多次。后者意味着它会在完成释放。yield yield

  2. 什么时候yield完成?

    yield其视为反向调用:控制权传递给调用者,而不是传递给被调用者。类似地,yield当控制权返回给它时完成,就像调用返回控制权一样。

请注意,两者withyield在这里按预期工作!a 的with lock目的是保护资源,并且在yield. 您始终可以显式释放此保护:

def safe_generator():
  while True:
    with lock():
      # keep lock for critical operation
      result = protected_operation()
    # release lock before releasing control
    yield result
于 2018-01-19T15:03:36.820 回答
0

那将是我期望的工作方式。是的,块在完成之前不会释放它的资源,所以从这个意义上说,资源已经逃脱了它的词法嵌套。但是,这与尝试在 with 块中使用相同资源的函数调用没有什么不同 - 在块尚未终止的情况下无论出于何种原因,都没有任何帮助。它并不是特定于生成器的任何东西。

不过,可能值得担心的一件事是如果生成器永远不会恢复,那么它的行为。我原以为该with块会像finally块一样工作并在终止时调用该__exit__部分,但事实并非如此。

于 2009-03-26T10:19:42.963 回答