资源-从Python的“ with”块中产生安全吗?(为什么)?
协程与资源获取的结合似乎会带来一些意想不到的(或不直观的)后果。
基本的问题是这样的事情是否有效:
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()
我真的不明白您要问的是什么冲突,也不是这个示例的问题:对同一个文件有两个并存的独立句柄是很好的。
我不知道我是在回答您的问题时得知,在生成器上有一个新的close()方法:
with
在生成器内部引发新的__exit__
异常,以终止迭代。 收到此异常后,生成器的代码必须引发finally
或try...finally
。当生成器被垃圾回收时,将调用
with
,因此这意味着生成器的代码在生成器被销毁之前获得了运行的最后机会。 最后的机会意味着现在可以保证生成器中的__exit__
语句可以正常工作; 现在,finally
子句将始终有运行的机会。 这似乎只是一小部分语言琐事,但实际上必须使用生成器和try...finally
才能实现PEP 343所述的with
语句。[HTTP://docs.Python.org/what是new/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!
我认为没有真正的冲突。 您只需要知道生成器就像其他任何拥有资源的对象一样,因此创建者有责任确保其正确完成(并避免与该对象所拥有的资源发生冲突/死锁)。 我在这里看到的唯一(次要)问题是生成器未实现上下文管理协议(至少从Python 2.5开始),因此您不能仅仅:
with coroutine() as cr:
doSomething(cr)
但必须:
cr = coroutine()
try:
doSomething(cr)
finally:
cr.close()
垃圾收集器还是会执行close()
,但是依靠它来释放资源是一个坏习惯。
因为close()
可以执行任意代码,所以我非常警惕在yield语句上持有锁。 但是,您可以通过许多其他方式获得类似的效果,包括调用可能已被覆盖或以其他方式修改的方法或函数。
但是,生成器总是(几乎总是)“关闭”,要么通过显式close()
调用,要么只是被垃圾回收。 关闭生成器会在生成器内部引发GeneratorExit
异常,并因此运行带有语句清除等的finally子句。您可以捕获该异常,但必须抛出或退出该函数(即抛出StopIteration
异常),而不是屈服。 在您编写的情况下,依靠垃圾收集器关闭生成器可能是一种不好的做法,因为这可能比您想要的要晚,并且如果有人调用sys._exit(),那么您的清理可能根本不会发生 。
那就是我期望事情会发生的方式。 是的,该块在完成之前不会释放其资源,因此从某种意义上说,资源已摆脱了其词法嵌套。 但是,这与尝试使用with块中的相同资源进行函数调用没有什么不同-在由于某种原因导致该块尚未终止的情况下,没有任何帮助。 并不是真正针对发电机的任何东西。
不过,可能值得担心的一件事是,如果永不恢复生成器,则会出现这种情况。 我本以为with
块会像finally
块一样工作,并在终止时调用__exit__
部分,但事实并非如此。
对于TLDR,请按以下方式查看:
with Context():
yield 1
pass # explicitly do nothing *after* yield
# exit context after explicitly doing nothing
with
在完成yield
(即不执行任何操作)后结束,with lock
在yield
完成(即恢复执行)之后执行。 因此,在yield
恢复控制之后,with
结束。
TLDR:当yield
释放控制权时,仍保留with
上下文。
实际上,这里只有两个相关的规则:
with
什么时候释放资源?它会在完成其块之后立即执行一次。 前者意味着它不会在
with
期间释放,因为这可能会发生多次。 后者意味着它在yield
完成后会释放。with
什么时候完成?将
with
视为反向呼叫:控制权传递给了呼叫者,而不是传递给被呼叫者。 同样,将控制传递回给它时,yield
完成,就像调用返回控制时一样。
请注意,with
和yield
均按此处预期的方式工作! 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