diff --git a/Lib/asyncio/threads.py b/Lib/asyncio/threads.py index db048a8231de16..22ba18f8ee66a3 100644 --- a/Lib/asyncio/threads.py +++ b/Lib/asyncio/threads.py @@ -21,5 +21,8 @@ async def to_thread(func, /, *args, **kwargs): """ loop = events.get_running_loop() ctx = contextvars.copy_context() - func_call = functools.partial(ctx.run, func, *args, **kwargs) - return await loop.run_in_executor(None, func_call) + if not ctx: + callback = functools.partial(func, *args, **kwargs) + else: + callback = functools.partial(ctx.run, func, *args, **kwargs) + return await loop.run_in_executor(None, callback) diff --git a/Lib/test/test_asyncio/test_threads.py b/Lib/test/test_asyncio/test_threads.py index c98c9a9b395ff9..d67413c081f0db 100644 --- a/Lib/test/test_asyncio/test_threads.py +++ b/Lib/test/test_asyncio/test_threads.py @@ -2,6 +2,7 @@ import asyncio import unittest +import functools from contextvars import ContextVar from unittest import mock @@ -61,6 +62,41 @@ def get_ctx(): self.assertEqual(result, 'parrot') + @mock.patch('asyncio.base_events.BaseEventLoop.run_in_executor') + async def test_to_thread_optimization_path(self, run_in_executor): + # This test ensures that `to_thread` uses the correct execution path + # based on whether the context is empty or not. + + # `to_thread` awaits the future returned by `run_in_executor`. + # We need to provide a completed future as a return value for the mock. + fut = asyncio.Future() + fut.set_result(None) + run_in_executor.return_value = fut + + def myfunc(): + pass + + # Test with an empty context (optimized path) + await asyncio.to_thread(myfunc) + run_in_executor.assert_called_once() + + callback = run_in_executor.call_args.args[1] + self.assertIsInstance(callback, functools.partial) + self.assertIs(callback.func, myfunc) + run_in_executor.reset_mock() + + # Test with a non-empty context (standard path) + var = ContextVar('var') + var.set('value') + + await asyncio.to_thread(myfunc) + run_in_executor.assert_called_once() + + callback = run_in_executor.call_args.args[1] + self.assertIsInstance(callback, functools.partial) + self.assertIsNot(callback.func, myfunc) # Should be ctx.run + self.assertIs(callback.args[0], myfunc) + if __name__ == "__main__": unittest.main() diff --git a/Misc/NEWS.d/next/Library/2025-07-01-02-19-00.gh-issue-136157.jGs_zV.rst b/Misc/NEWS.d/next/Library/2025-07-01-02-19-00.gh-issue-136157.jGs_zV.rst new file mode 100644 index 00000000000000..c6228c1f6e062d --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-07-01-02-19-00.gh-issue-136157.jGs_zV.rst @@ -0,0 +1 @@ +Optimized :func:`asyncio.to_thread` to avoid unnecessary performance overhead from calling :meth:`contextvars.Context.run` when the context is empty.