Skip to content

Commit 0f56263

Browse files
authored
bpo-32309: Add support for contextvars in asyncio.to_thread() (GH-20278)
Allows contextvars from the main thread to be accessed in the separate thread used in `asyncio.to_thread()`. See the [discussion](#20143 (comment)) in GH-20143 for context. Automerge-Triggered-By: @aeros
1 parent 7efb826 commit 0f56263

File tree

3 files changed

+25
-3
lines changed

3 files changed

+25
-3
lines changed

Doc/library/asyncio-task.rst

+5-1
Original file line numberDiff line numberDiff line change
@@ -610,7 +610,9 @@ Running in Threads
610610
Asynchronously run function *func* in a separate thread.
611611

612612
Any \*args and \*\*kwargs supplied for this function are directly passed
613-
to *func*.
613+
to *func*. Also, the current :class:`contextvars.Context` is propogated,
614+
allowing context variables from the event loop thread to be accessed in the
615+
separate thread.
614616

615617
Return an :class:`asyncio.Future` which represents the eventual result of
616618
*func*.
@@ -657,6 +659,8 @@ Running in Threads
657659
that release the GIL or alternative Python implementations that don't
658660
have one, `asyncio.to_thread()` can also be used for CPU-bound functions.
659661

662+
.. versionadded:: 3.9
663+
660664

661665
Scheduling From Other Threads
662666
=============================

Lib/asyncio/threads.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""High-level support for working with threads in asyncio"""
22

33
import functools
4+
import contextvars
45

56
from . import events
67

@@ -12,10 +13,13 @@ async def to_thread(func, /, *args, **kwargs):
1213
"""Asynchronously run function *func* in a separate thread.
1314
1415
Any *args and **kwargs supplied for this function are directly passed
15-
to *func*.
16+
to *func*. Also, the current :class:`contextvars.Context` is propogated,
17+
allowing context variables from the main thread to be accessed in the
18+
separate thread.
1619
1720
Return an asyncio.Future which represents the eventual result of *func*.
1821
"""
1922
loop = events.get_running_loop()
20-
func_call = functools.partial(func, *args, **kwargs)
23+
ctx = contextvars.copy_context()
24+
func_call = functools.partial(ctx.run, func, *args, **kwargs)
2125
return await loop.run_in_executor(None, func_call)

Lib/test/test_asyncio/test_threads.py

+14
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import asyncio
44
import unittest
55

6+
from contextvars import ContextVar
67
from unittest import mock
78
from test.test_asyncio import utils as test_utils
89

@@ -74,6 +75,19 @@ async def main():
7475
self.loop.run_until_complete(main())
7576
func.assert_called_once_with('test', something=True)
7677

78+
def test_to_thread_contextvars(self):
79+
test_ctx = ContextVar('test_ctx')
80+
81+
def get_ctx():
82+
return test_ctx.get()
83+
84+
async def main():
85+
test_ctx.set('parrot')
86+
return await asyncio.to_thread(get_ctx)
87+
88+
result = self.loop.run_until_complete(main())
89+
self.assertEqual(result, 'parrot')
90+
7791

7892
if __name__ == "__main__":
7993
unittest.main()

0 commit comments

Comments
 (0)