Skip to content

Commit 9b9520d

Browse files
Wankupiagronholm
andauthored
Fixed cyclic references in to_thread.run_sync() on asyncio (#887)
Co-authored-by: Alex Grönholm <[email protected]>
1 parent 1f04d6b commit 9b9520d

File tree

3 files changed

+40
-2
lines changed

3 files changed

+40
-2
lines changed

docs/versionhistory.rst

+3
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ This library adheres to `Semantic Versioning 2.0 <http://semver.org/>`_.
3333
- Fixed ``anyio.Path.iterdir()`` making a blocking call in Python 3.13
3434
(`#873 <https://github.com/agronholm/anyio/issues/873>`_; PR by @cbornet and
3535
@agronholm)
36+
- Fixed ``anyio.to_thread.run_sync()`` needlessly holding on to references of the
37+
context, function, arguments and others until the next work item on asyncio
38+
(PR by @Wankupi)
3639

3740
**4.8.0**
3841

src/anyio/_backends/_asyncio.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import array
44
import asyncio
55
import concurrent.futures
6+
import contextvars
67
import math
78
import os
89
import socket
@@ -974,7 +975,10 @@ def run(self) -> None:
974975
self._report_result, future, result, exception
975976
)
976977

978+
del result, exception
979+
977980
self.queue.task_done()
981+
del item, context, func, args, future, cancel_scope
978982

979983
def stop(self, f: asyncio.Task | None = None) -> None:
980984
self.stopping = True
@@ -2433,7 +2437,9 @@ async def run_sync_in_worker_thread( # type: ignore[return]
24332437
worker = WorkerThread(root_task, workers, idle_workers)
24342438
worker.start()
24352439
workers.add(worker)
2436-
root_task.add_done_callback(worker.stop)
2440+
root_task.add_done_callback(
2441+
worker.stop, context=contextvars.Context()
2442+
)
24372443
else:
24382444
worker = idle_workers.pop()
24392445

tests/test_to_thread.py

+30-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from __future__ import annotations
22

33
import asyncio
4+
import gc
5+
import sys
46
import threading
57
import time
68
from concurrent.futures import Future, ThreadPoolExecutor
@@ -23,7 +25,7 @@
2325
)
2426
from anyio.from_thread import BlockingPortalProvider
2527

26-
from .conftest import asyncio_params
28+
from .conftest import asyncio_params, no_other_refs
2729

2830
pytestmark = pytest.mark.anyio
2931

@@ -360,3 +362,30 @@ def dummy() -> None:
360362
portal.call(event.set)
361363

362364
assert len(threads) == 1
365+
366+
367+
@pytest.mark.skipif(
368+
sys.implementation.name == "pypy",
369+
reason=(
370+
"gc.get_referrers is broken on PyPy (see "
371+
"https://github.com/pypy/pypy/issues/5075)"
372+
),
373+
)
374+
async def test_run_sync_worker_cyclic_references() -> None:
375+
class Foo:
376+
pass
377+
378+
def foo(_: Foo) -> None:
379+
pass
380+
381+
cvar = ContextVar[Foo]("cvar")
382+
contextval = Foo()
383+
arg = Foo()
384+
cvar.set(contextval)
385+
await to_thread.run_sync(foo, arg)
386+
cvar.set(Foo())
387+
gc.collect()
388+
389+
assert gc.get_referrers(contextval) == no_other_refs()
390+
assert gc.get_referrers(foo) == no_other_refs()
391+
assert gc.get_referrers(arg) == no_other_refs()

0 commit comments

Comments
 (0)