Skip to content

Commit c880ffe

Browse files
twisteroidambassador1st1
authored andcommitted
bpo-34769: Thread safety for _asyncgen_finalizer_hook(). (GH-9716)
1 parent 79d2133 commit c880ffe

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
@@ -477,10 +477,7 @@ def _check_closed(self):
477477
def _asyncgen_finalizer_hook(self, agen):
478478
self._asyncgens.discard(agen)
479479
if not self.is_closed():
480-
self.create_task(agen.aclose())
481-
# Wake up the loop if the finalizer was called from
482-
# a different thread.
483-
self._write_to_self()
480+
self.call_soon_threadsafe(self.create_task, agen.aclose())
484481

485482
def _asyncgen_firstiter_hook(self, agen):
486483
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
@@ -926,6 +926,74 @@ def test_run_forever_pre_stopped(self):
926926
self.loop.run_forever()
927927
self.loop._selector.select.assert_called_once_with(0)
928928

929+
async def leave_unfinalized_asyncgen(self):
930+
# Create an async generator, iterate it partially, and leave it
931+
# to be garbage collected.
932+
# Used in async generator finalization tests.
933+
# Depends on implementation details of garbage collector. Changes
934+
# in gc may break this function.
935+
status = {'started': False,
936+
'stopped': False,
937+
'finalized': False}
938+
939+
async def agen():
940+
status['started'] = True
941+
try:
942+
for item in ['ZERO', 'ONE', 'TWO', 'THREE', 'FOUR']:
943+
yield item
944+
finally:
945+
status['finalized'] = True
946+
947+
ag = agen()
948+
ai = ag.__aiter__()
949+
950+
async def iter_one():
951+
try:
952+
item = await ai.__anext__()
953+
except StopAsyncIteration:
954+
return
955+
if item == 'THREE':
956+
status['stopped'] = True
957+
return
958+
asyncio.create_task(iter_one())
959+
960+
asyncio.create_task(iter_one())
961+
return status
962+
963+
def test_asyncgen_finalization_by_gc(self):
964+
# Async generators should be finalized when garbage collected.
965+
self.loop._process_events = mock.Mock()
966+
self.loop._write_to_self = mock.Mock()
967+
with support.disable_gc():
968+
status = self.loop.run_until_complete(self.leave_unfinalized_asyncgen())
969+
while not status['stopped']:
970+
test_utils.run_briefly(self.loop)
971+
self.assertTrue(status['started'])
972+
self.assertTrue(status['stopped'])
973+
self.assertFalse(status['finalized'])
974+
support.gc_collect()
975+
test_utils.run_briefly(self.loop)
976+
self.assertTrue(status['finalized'])
977+
978+
def test_asyncgen_finalization_by_gc_in_other_thread(self):
979+
# Python issue 34769: If garbage collector runs in another
980+
# thread, async generators will not finalize in debug
981+
# mode.
982+
self.loop._process_events = mock.Mock()
983+
self.loop._write_to_self = mock.Mock()
984+
self.loop.set_debug(True)
985+
with support.disable_gc():
986+
status = self.loop.run_until_complete(self.leave_unfinalized_asyncgen())
987+
while not status['stopped']:
988+
test_utils.run_briefly(self.loop)
989+
self.assertTrue(status['started'])
990+
self.assertTrue(status['stopped'])
991+
self.assertFalse(status['finalized'])
992+
self.loop.run_until_complete(
993+
self.loop.run_in_executor(None, support.gc_collect))
994+
test_utils.run_briefly(self.loop)
995+
self.assertTrue(status['finalized'])
996+
929997

930998
class MyProto(asyncio.Protocol):
931999
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)