Skip to content

Commit df9f12f

Browse files
committed
Optimize asyncio.to_thread to avoid contextvars.copy_context() overhead for empty contexts
1 parent 23caccf commit df9f12f

File tree

2 files changed

+42
-3
lines changed

2 files changed

+42
-3
lines changed

Lib/asyncio/threads.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,8 @@ async def to_thread(func, /, *args, **kwargs):
2121
"""
2222
loop = events.get_running_loop()
2323
ctx = contextvars.copy_context()
24-
func_call = functools.partial(ctx.run, func, *args, **kwargs)
25-
return await loop.run_in_executor(None, func_call)
24+
if len(ctx) == 0:
25+
callback = functools.partial(func, *args, **kwargs)
26+
else:
27+
callback = functools.partial(ctx.run, func, *args, **kwargs)
28+
return await loop.run_in_executor(None, callback)

Lib/test/test_asyncio/test_threads.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@
22

33
import asyncio
44
import unittest
5+
import functools
56

67
from contextvars import ContextVar
78
from unittest import mock
89

910

1011
def tearDownModule():
11-
asyncio._set_event_loop_policy(None)
12+
asyncio.set_event_loop_policy(None)
1213

1314

1415
class ToThreadTests(unittest.IsolatedAsyncioTestCase):
@@ -61,6 +62,41 @@ def get_ctx():
6162

6263
self.assertEqual(result, 'parrot')
6364

65+
@mock.patch('asyncio.base_events.BaseEventLoop.run_in_executor')
66+
async def test_to_thread_optimization_path(self, run_in_executor):
67+
# This test ensures that `to_thread` uses the correct execution path
68+
# based on whether the context is empty or not.
69+
70+
# `to_thread` awaits the future returned by `run_in_executor`.
71+
# We need to provide a completed future as a return value for the mock.
72+
fut = asyncio.Future()
73+
fut.set_result(None)
74+
run_in_executor.return_value = fut
75+
76+
def myfunc():
77+
pass
78+
79+
# Test with an empty context (optimized path)
80+
await asyncio.to_thread(myfunc)
81+
run_in_executor.assert_called_once()
82+
83+
callback = run_in_executor.call_args.args[1]
84+
self.assertIsInstance(callback, functools.partial)
85+
self.assertIs(callback.func, myfunc)
86+
run_in_executor.reset_mock()
87+
88+
# Test with a non-empty context (standard path)
89+
var = ContextVar('var')
90+
var.set('value')
91+
92+
await asyncio.to_thread(myfunc)
93+
run_in_executor.assert_called_once()
94+
95+
callback = run_in_executor.call_args.args[1]
96+
self.assertIsInstance(callback, functools.partial)
97+
self.assertIsNot(callback.func, myfunc) # Should be ctx.run
98+
self.assertIs(callback.args[0], myfunc)
99+
64100

65101
if __name__ == "__main__":
66102
unittest.main()

0 commit comments

Comments
 (0)