Skip to content

Commit 41e5ec3

Browse files
bpo-34769: Thread safety for _asyncgen_finalizer_hook(). (GH-9716)
(cherry picked from commit c880ffe) Co-authored-by: twisteroid ambassador <[email protected]>
1 parent 0ce31d3 commit 41e5ec3

File tree

3 files changed

+71
-4
lines changed

3 files changed

+71
-4
lines changed

Lib/asyncio/base_events.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -471,10 +471,7 @@ def _check_closed(self):
471471
def _asyncgen_finalizer_hook(self, agen):
472472
self._asyncgens.discard(agen)
473473
if not self.is_closed():
474-
self.create_task(agen.aclose())
475-
# Wake up the loop if the finalizer was called from
476-
# a different thread.
477-
self._write_to_self()
474+
self.call_soon_threadsafe(self.create_task, agen.aclose())
478475

479476
def _asyncgen_firstiter_hook(self, agen):
480477
if self._asyncgens_shutdown_called:

Lib/test/test_asyncio/test_base_events.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -907,6 +907,74 @@ def test_run_forever_pre_stopped(self):
907907
self.loop.run_forever()
908908
self.loop._selector.select.assert_called_once_with(0)
909909

910+
async def leave_unfinalized_asyncgen(self):
911+
# Create an async generator, iterate it partially, and leave it
912+
# to be garbage collected.
913+
# Used in async generator finalization tests.
914+
# Depends on implementation details of garbage collector. Changes
915+
# in gc may break this function.
916+
status = {'started': False,
917+
'stopped': False,
918+
'finalized': False}
919+
920+
async def agen():
921+
status['started'] = True
922+
try:
923+
for item in ['ZERO', 'ONE', 'TWO', 'THREE', 'FOUR']:
924+
yield item
925+
finally:
926+
status['finalized'] = True
927+
928+
ag = agen()
929+
ai = ag.__aiter__()
930+
931+
async def iter_one():
932+
try:
933+
item = await ai.__anext__()
934+
except StopAsyncIteration:
935+
return
936+
if item == 'THREE':
937+
status['stopped'] = True
938+
return
939+
asyncio.create_task(iter_one())
940+
941+
asyncio.create_task(iter_one())
942+
return status
943+
944+
def test_asyncgen_finalization_by_gc(self):
945+
# Async generators should be finalized when garbage collected.
946+
self.loop._process_events = mock.Mock()
947+
self.loop._write_to_self = mock.Mock()
948+
with support.disable_gc():
949+
status = self.loop.run_until_complete(self.leave_unfinalized_asyncgen())
950+
while not status['stopped']:
951+
test_utils.run_briefly(self.loop)
952+
self.assertTrue(status['started'])
953+
self.assertTrue(status['stopped'])
954+
self.assertFalse(status['finalized'])
955+
support.gc_collect()
956+
test_utils.run_briefly(self.loop)
957+
self.assertTrue(status['finalized'])
958+
959+
def test_asyncgen_finalization_by_gc_in_other_thread(self):
960+
# Python issue 34769: If garbage collector runs in another
961+
# thread, async generators will not finalize in debug
962+
# mode.
963+
self.loop._process_events = mock.Mock()
964+
self.loop._write_to_self = mock.Mock()
965+
self.loop.set_debug(True)
966+
with support.disable_gc():
967+
status = self.loop.run_until_complete(self.leave_unfinalized_asyncgen())
968+
while not status['stopped']:
969+
test_utils.run_briefly(self.loop)
970+
self.assertTrue(status['started'])
971+
self.assertTrue(status['stopped'])
972+
self.assertFalse(status['finalized'])
973+
self.loop.run_until_complete(
974+
self.loop.run_in_executor(None, support.gc_collect))
975+
test_utils.run_briefly(self.loop)
976+
self.assertTrue(status['finalized'])
977+
910978

911979
class MyProto(asyncio.Protocol):
912980
done = None
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix for async generators not finalizing when event loop is in debug mode and
2+
garbage collector runs in another thread.

0 commit comments

Comments
 (0)