diff --git a/Doc/library/asyncio-eventloop.rst b/Doc/library/asyncio-eventloop.rst index 045787e0ce7875..7633e8a823b3b2 100644 --- a/Doc/library/asyncio-eventloop.rst +++ b/Doc/library/asyncio-eventloop.rst @@ -167,13 +167,20 @@ Running and stopping the loop .. versionadded:: 3.6 -.. coroutinemethod:: loop.shutdown_default_executor() +.. coroutinemethod:: loop.shutdown_default_executor(timeout=None) Schedule the closure of the default executor and wait for it to join all of the threads in the :class:`ThreadPoolExecutor`. After calling this method, a :exc:`RuntimeError` will be raised if :meth:`loop.run_in_executor` is called while using the default executor. + The *timeout* parameter specifies the amount of time the threadpool will + be given to finish joining. The default value is ``None``, which means the + threadpool will be given an indefinite amount of time. + + If the timeout duration is reached, a warning is emitted and threadpool is + terminated without waiting for its threads to finish joining. + Note that there is no need to call this function when :func:`asyncio.run` is used. diff --git a/Doc/library/asyncio-task.rst b/Doc/library/asyncio-task.rst index 155887a3ab3af9..0ed68ceccdb370 100644 --- a/Doc/library/asyncio-task.rst +++ b/Doc/library/asyncio-task.rst @@ -227,6 +227,10 @@ Running an asyncio Program the end. It should be used as a main entry point for asyncio programs, and should ideally only be called once. + The threadpool is given a timeout duration of 5 minutes to join its threads. + If the threadpool hasn't finishing joining within that duration, a warning is + emitted and the threadpool is closed. + Example:: async def main(): diff --git a/Lib/asyncio/base_events.py b/Lib/asyncio/base_events.py index 031071281b38f7..d93ba634c2bba1 100644 --- a/Lib/asyncio/base_events.py +++ b/Lib/asyncio/base_events.py @@ -549,8 +549,13 @@ async def shutdown_asyncgens(self): 'asyncgen': agen }) - async def shutdown_default_executor(self): - """Schedule the shutdown of the default executor.""" + async def shutdown_default_executor(self, timeout=None): + """Schedule the shutdown of the default executor. + + The timeout parameter specifies the amount of time the threadpool will + be given to finish joining. The default value is None, which means + that the threadpool will be given an indefinite amount of time. + """ self._executor_shutdown_called = True if self._default_executor is None: return @@ -560,7 +565,13 @@ async def shutdown_default_executor(self): try: await future finally: - thread.join() + thread.join(timeout) + + if thread.is_alive(): + warnings.warn("The ThreadPoolExecutor did not finishing joining " + f"its threads within {timeout} seconds.", + RuntimeWarning, stacklevel=2) + self._default_executor.shutdown(wait=False) def _do_shutdown(self, future): try: diff --git a/Lib/asyncio/runners.py b/Lib/asyncio/runners.py index 03ce33300eba83..3ac88777faeee1 100644 --- a/Lib/asyncio/runners.py +++ b/Lib/asyncio/runners.py @@ -5,12 +5,16 @@ from . import tasks +# Default timeout for joining the threads in the threadpool +THREAD_JOIN_TIMEOUT = 300 + + def run(main, *, debug=False): """Execute the coroutine and return the result. This function runs the passed coroutine, taking care of - managing the asyncio event loop and finalizing asynchronous - generators. + managing the asyncio event loop, finalizing asynchronous + generators, and closing the threadpool. This function cannot be called when another asyncio event loop is running in the same thread. @@ -21,6 +25,9 @@ def run(main, *, debug=False): It should be used as a main entry point for asyncio programs, and should ideally only be called once. + The threadpool is given a timeout duration of 5 minutes. If its threads + haven't finishing joining within that duration, the threadpool is terminated. + Example: async def main(): @@ -45,7 +52,8 @@ async def main(): try: _cancel_all_tasks(loop) loop.run_until_complete(loop.shutdown_asyncgens()) - loop.run_until_complete(loop.shutdown_default_executor()) + loop.run_until_complete( + loop.shutdown_default_executor(timeout=THREAD_JOIN_TIMEOUT)) finally: events.set_event_loop(None) loop.close() diff --git a/Misc/NEWS.d/next/Library/2019-09-25-00-37-51.bpo-38267.X9Jb5V.rst b/Misc/NEWS.d/next/Library/2019-09-25-00-37-51.bpo-38267.X9Jb5V.rst new file mode 100644 index 00000000000000..79c84ea0ae9f90 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2019-09-25-00-37-51.bpo-38267.X9Jb5V.rst @@ -0,0 +1 @@ +Add *timeout* parameter to :meth:`asyncio.loop.shutdown_default_executor`.