The combination of coroutines and resource acquisition seems like it could have some unintended (or unintuitive) consequences.
The basic question is whether or not somet
For a TLDR, look at it this way:
with Context():
yield 1
pass # explicitly do nothing *after* yield
# exit context after explicitly doing nothing
The Context
ends after pass
is done (i.e. nothing), pass
executes after yield
is done (i.e. execution resumes). So, the with
ends after control is resumed at yield
.
TLDR: A with
context remains held when yield
releases control.
There are actually just two rules that are relevant here:
When does with
release its resource?
It does so once and directly after its block is done. The former means it does not release during a yield
, as that could happen several times. The later means it does release after yield
has completed.
When does yield
complete?
Think of yield
as a reverse call: control is passed up to a caller, not down to a called one. Similarly, yield
completes when control is passed back to it, just like when a call returns control.
Note that both with
and yield
are working as intended here! The point of a with lock
is to protect a resource and it stays protected during a yield
. You can always explicitly release this protection:
def safe_generator():
while True:
with lock():
# keep lock for critical operation
result = protected_operation()
# release lock before releasing control
yield result