From df9f12f9f20415aea32b4adfc46a4fdf57d2db58 Mon Sep 17 00:00:00 2001 From: heliang <3596006474@qq.com> Date: Tue, 1 Jul 2025 09:54:48 +0800 Subject: [PATCH 1/8] Optimize asyncio.to_thread to avoid contextvars.copy_context() overhead for empty contexts --- Lib/asyncio/threads.py | 7 +++-- Lib/test/test_asyncio/test_threads.py | 38 ++++++++++++++++++++++++++- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/Lib/asyncio/threads.py b/Lib/asyncio/threads.py index db048a8231de16..b1b030bf2c0421 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 len(ctx) == 0: + 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..a1323c4dbdbb21 100644 --- a/Lib/test/test_asyncio/test_threads.py +++ b/Lib/test/test_asyncio/test_threads.py @@ -2,13 +2,14 @@ import asyncio import unittest +import functools from contextvars import ContextVar from unittest import mock def tearDownModule(): - asyncio._set_event_loop_policy(None) + asyncio.set_event_loop_policy(None) class ToThreadTests(unittest.IsolatedAsyncioTestCase): @@ -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() From 5c758d5d54e27f0a73d9d56820f3ff64f7aa471c Mon Sep 17 00:00:00 2001 From: heliang <3596006474@qq.com> Date: Tue, 1 Jul 2025 10:15:24 +0800 Subject: [PATCH 2/8] Fix DeprecationWarning in asyncio tests --- Lib/test/test_asyncio/test_threads.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_asyncio/test_threads.py b/Lib/test/test_asyncio/test_threads.py index a1323c4dbdbb21..c527138fca37c4 100644 --- a/Lib/test/test_asyncio/test_threads.py +++ b/Lib/test/test_asyncio/test_threads.py @@ -3,13 +3,16 @@ import asyncio import unittest import functools +import warnings from contextvars import ContextVar from unittest import mock def tearDownModule(): - asyncio.set_event_loop_policy(None) + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + asyncio.set_event_loop_policy(None) class ToThreadTests(unittest.IsolatedAsyncioTestCase): From 9ed2acb877ef9f82cb6e298308796d8bd3b914b0 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Tue, 1 Jul 2025 02:19:01 +0000 Subject: [PATCH 3/8] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next/Library/2025-07-01-02-19-00.gh-issue-136157.jGs_zV.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2025-07-01-02-19-00.gh-issue-136157.jGs_zV.rst 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..08a8c20a9365f4 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-07-01-02-19-00.gh-issue-136157.jGs_zV.rst @@ -0,0 +1 @@ +Optimized ``asyncio.to_thread`` to avoid unnecessary performance overhead from calling ``contextvars.copy_context().run`` when the context is empty. From 56035d4a921b291eb9da8bd73b053319a94e943a Mon Sep 17 00:00:00 2001 From: heliang <3596006474@qq.com> Date: Tue, 1 Jul 2025 10:20:42 +0800 Subject: [PATCH 4/8] Use private _set_event_loop_policy API to avoid DeprecationWarning --- Lib/test/test_asyncio/test_threads.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Lib/test/test_asyncio/test_threads.py b/Lib/test/test_asyncio/test_threads.py index c527138fca37c4..d67413c081f0db 100644 --- a/Lib/test/test_asyncio/test_threads.py +++ b/Lib/test/test_asyncio/test_threads.py @@ -3,16 +3,13 @@ import asyncio import unittest import functools -import warnings from contextvars import ContextVar from unittest import mock def tearDownModule(): - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) - asyncio.set_event_loop_policy(None) + asyncio._set_event_loop_policy(None) class ToThreadTests(unittest.IsolatedAsyncioTestCase): From 870f8188a82c2f22ef694153afd5edf5b1fd5a69 Mon Sep 17 00:00:00 2001 From: heliang <3596006474@qq.com> Date: Tue, 1 Jul 2025 17:43:32 +0800 Subject: [PATCH 5/8] use bool to replace len() --- Lib/asyncio/threads.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/asyncio/threads.py b/Lib/asyncio/threads.py index b1b030bf2c0421..22ba18f8ee66a3 100644 --- a/Lib/asyncio/threads.py +++ b/Lib/asyncio/threads.py @@ -21,7 +21,7 @@ async def to_thread(func, /, *args, **kwargs): """ loop = events.get_running_loop() ctx = contextvars.copy_context() - if len(ctx) == 0: + if not ctx: callback = functools.partial(func, *args, **kwargs) else: callback = functools.partial(ctx.run, func, *args, **kwargs) From 28622dcf4cea414d1456fe003ef87c3d1ed97dfb Mon Sep 17 00:00:00 2001 From: heliang666s <147408835+heliang666s@users.noreply.github.com> Date: Wed, 2 Jul 2025 01:13:41 +0800 Subject: [PATCH 6/8] Update Misc/NEWS.d/next/Library/2025-07-01-02-19-00.gh-issue-136157.jGs_zV.rst Co-authored-by: Peter Bierma --- .../next/Library/2025-07-01-02-19-00.gh-issue-136157.jGs_zV.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 08a8c20a9365f4..a963e14e79730f 100644 --- 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 @@ -1 +1 @@ -Optimized ``asyncio.to_thread`` to avoid unnecessary performance overhead from calling ``contextvars.copy_context().run`` when the context is empty. +Optimized :func:`asyncio.to_thread``to avoid unnecessary performance overhead from calling :meth:`contextvars.Context.run`` when the context is empty. From 5d0582db1f2da0f4784021ee10573abd9cd5ea5b Mon Sep 17 00:00:00 2001 From: heliang666s <147408835+heliang666s@users.noreply.github.com> Date: Wed, 2 Jul 2025 01:27:55 +0800 Subject: [PATCH 7/8] Update 2025-07-01-02-19-00.gh-issue-136157.jGs_zV.rst --- .../next/Library/2025-07-01-02-19-00.gh-issue-136157.jGs_zV.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index a963e14e79730f..6bd7fd886fff97 100644 --- 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 @@ -1 +1 @@ -Optimized :func:`asyncio.to_thread``to avoid unnecessary performance overhead from calling :meth:`contextvars.Context.run`` when the context is empty. +Optimized :func:`asyncio.to_thread`to avoid unnecessary performance overhead from calling :meth:`contextvars.Context.run` when the context is empty. From ac322ea4b62afdc94cf7d84076776229decbaaa8 Mon Sep 17 00:00:00 2001 From: heliang666s <147408835+heliang666s@users.noreply.github.com> Date: Wed, 2 Jul 2025 02:11:15 +0800 Subject: [PATCH 8/8] Update 2025-07-01-02-19-00.gh-issue-136157.jGs_zV.rst --- .../next/Library/2025-07-01-02-19-00.gh-issue-136157.jGs_zV.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 6bd7fd886fff97..c6228c1f6e062d 100644 --- 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 @@ -1 +1 @@ -Optimized :func:`asyncio.to_thread`to avoid unnecessary performance overhead from calling :meth:`contextvars.Context.run` when the context is empty. +Optimized :func:`asyncio.to_thread` to avoid unnecessary performance overhead from calling :meth:`contextvars.Context.run` when the context is empty.