Skip to content

Commit a14dda5

Browse files
twisteroidambassador1st1
authored andcommitted
[3.6] bpo-34769: Thread safety for _asyncgen_finalizer_hook(). (GH-9716) (GH-9792)
1 parent e7ebf14 commit a14dda5

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
@@ -368,10 +368,7 @@ def _check_closed(self):
368368
def _asyncgen_finalizer_hook(self, agen):
369369
self._asyncgens.discard(agen)
370370
if not self.is_closed():
371-
self.create_task(agen.aclose())
372-
# Wake up the loop if the finalizer was called from
373-
# a different thread.
374-
self._write_to_self()
371+
self.call_soon_threadsafe(self.create_task, agen.aclose())
375372

376373
def _asyncgen_firstiter_hook(self, agen):
377374
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
@@ -915,6 +915,74 @@ def test_run_forever_pre_stopped(self):
915915
self.loop.run_forever()
916916
self.loop._selector.select.assert_called_once_with(0)
917917

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

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