Skip to content

Commit 9fdc64c

Browse files
aerosasvetlov
authored andcommitted
bpo-34037: Fix test_asyncio failure and add loop.shutdown_default_executor() (GH-15735)
1 parent 3171d67 commit 9fdc64c

File tree

6 files changed

+54
-2
lines changed

6 files changed

+54
-2
lines changed

Doc/library/asyncio-eventloop.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,18 @@ Running and stopping the loop
167167

168168
.. versionadded:: 3.6
169169

170+
.. coroutinemethod:: loop.shutdown_default_executor()
171+
172+
Schedule the closure of the default executor and wait for it to join all of
173+
the threads in the :class:`ThreadPoolExecutor`. After calling this method, a
174+
:exc:`RuntimeError` will be raised if :meth:`loop.run_in_executor` is called
175+
while using the default executor.
176+
177+
Note that there is no need to call this function when
178+
:func:`asyncio.run` is used.
179+
180+
.. versionadded:: 3.9
181+
170182

171183
Scheduling callbacks
172184
^^^^^^^^^^^^^^^^^^^^

Doc/library/asyncio-task.rst

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -213,8 +213,8 @@ Running an asyncio Program
213213
.. function:: run(coro, \*, debug=False)
214214

215215
This function runs the passed coroutine, taking care of
216-
managing the asyncio event loop and *finalizing asynchronous
217-
generators*.
216+
managing the asyncio event loop, *finalizing asynchronous
217+
generators*, and closing the threadpool.
218218

219219
This function cannot be called when another asyncio event loop is
220220
running in the same thread.
@@ -229,6 +229,8 @@ Running an asyncio Program
229229
**Important:** this function has been added to asyncio in
230230
Python 3.7 on a :term:`provisional basis <provisional api>`.
231231

232+
.. versionchanged:: 3.9
233+
Updated to use :meth:`loop.shutdown_default_executor`.
232234

233235
Creating Tasks
234236
==============

Lib/asyncio/base_events.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,8 @@ def __init__(self):
406406
self._asyncgens = weakref.WeakSet()
407407
# Set to True when `loop.shutdown_asyncgens` is called.
408408
self._asyncgens_shutdown_called = False
409+
# Set to True when `loop.shutdown_default_executor` is called.
410+
self._executor_shutdown_called = False
409411

410412
def __repr__(self):
411413
return (
@@ -503,6 +505,10 @@ def _check_closed(self):
503505
if self._closed:
504506
raise RuntimeError('Event loop is closed')
505507

508+
def _check_default_executor(self):
509+
if self._executor_shutdown_called:
510+
raise RuntimeError('Executor shutdown has been called')
511+
506512
def _asyncgen_finalizer_hook(self, agen):
507513
self._asyncgens.discard(agen)
508514
if not self.is_closed():
@@ -543,6 +549,26 @@ async def shutdown_asyncgens(self):
543549
'asyncgen': agen
544550
})
545551

552+
async def shutdown_default_executor(self):
553+
"""Schedule the shutdown of the default executor."""
554+
self._executor_shutdown_called = True
555+
if self._default_executor is None:
556+
return
557+
future = self.create_future()
558+
thread = threading.Thread(target=self._do_shutdown, args=(future,))
559+
thread.start()
560+
try:
561+
await future
562+
finally:
563+
thread.join()
564+
565+
def _do_shutdown(self, future):
566+
try:
567+
self._default_executor.shutdown(wait=True)
568+
self.call_soon_threadsafe(future.set_result, None)
569+
except Exception as ex:
570+
self.call_soon_threadsafe(future.set_exception, ex)
571+
546572
def run_forever(self):
547573
"""Run until stop() is called."""
548574
self._check_closed()
@@ -632,6 +658,7 @@ def close(self):
632658
self._closed = True
633659
self._ready.clear()
634660
self._scheduled.clear()
661+
self._executor_shutdown_called = True
635662
executor = self._default_executor
636663
if executor is not None:
637664
self._default_executor = None
@@ -768,6 +795,8 @@ def run_in_executor(self, executor, func, *args):
768795
self._check_callback(func, 'run_in_executor')
769796
if executor is None:
770797
executor = self._default_executor
798+
# Only check when the default executor is being used
799+
self._check_default_executor()
771800
if executor is None:
772801
executor = concurrent.futures.ThreadPoolExecutor()
773802
self._default_executor = executor

Lib/asyncio/events.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,10 @@ async def shutdown_asyncgens(self):
249249
"""Shutdown all active asynchronous generators."""
250250
raise NotImplementedError
251251

252+
async def shutdown_default_executor(self):
253+
"""Schedule the shutdown of the default executor."""
254+
raise NotImplementedError
255+
252256
# Methods scheduling callbacks. All these return Handles.
253257

254258
def _timer_handle_cancelled(self, handle):

Lib/asyncio/runners.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ async def main():
4545
try:
4646
_cancel_all_tasks(loop)
4747
loop.run_until_complete(loop.shutdown_asyncgens())
48+
loop.run_until_complete(loop.shutdown_default_executor())
4849
finally:
4950
events.set_event_loop(None)
5051
loop.close()
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
For :mod:`asyncio`, add a new coroutine :meth:`loop.shutdown_default_executor`.
2+
The new coroutine provides an API to schedule an executor shutdown that waits
3+
on the threadpool to finish closing. Also, :func:`asyncio.run` has been updated
4+
to utilize the new coroutine. Patch by Kyle Stanley.

0 commit comments

Comments
 (0)