Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@
in package metadata.
https://github.com/joblib/threadpoolctl/issues/251

- Faster performance when creating a `ThreadpoolController`.
https://github.com/joblib/threadpoolctl/pull/249

- Shared libraries are now cached across runs, whereas previously they would be
unloaded after the `ThreadpoolController` was garbage collected.
https://github.com/joblib/threadpoolctl/pull/249

- A single shared library can no longer have multiple `LibController` instances.
https://github.com/joblib/threadpoolctl/pull/249

3.7.0 (2026-09-15)
==================

Expand Down
2 changes: 1 addition & 1 deletion benchmarks/bench_context_manager_overhead.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,4 @@
pass
timings.append(time.time() - t)

print(f"Overhead per call: {mean(timings) * 1e3:.3f} +/-{stdev(timings) * 1e3:.3f} ms")
print(f"Overhead per call: {mean(timings) * 1e3:.4f} +/-{stdev(timings) * 1e3:.4f} ms")
59 changes: 59 additions & 0 deletions tests/test_cache.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"""Tests for ``threadpoolctl``'s internal caching layer."""

from ctypes import CDLL

import pytest

from threadpoolctl import ThreadpoolController, _CDLL_CACHE


def test_cdlls_are_cached():
"""
When a ``LibController`` is created, it reuses the same ``CDLL``.
"""
pytest.importorskip("numpy")

controller = ThreadpoolController()
if not controller.lib_controllers:
pytest.skip("No libraries loaded")

cached_cdll = controller.lib_controllers[0].dynlib
assert isinstance(cached_cdll, CDLL)

controller2 = ThreadpoolController()
assert cached_cdll is controller2.lib_controllers[0].dynlib


def test_cache_methods_on_dynlib(request):
"""
``_CDLLCache.cache_method_on_dynlib()`` caches the result, tied to the
underlying ``CDLL`` as an invalidation key.
"""
pytest.importorskip("numpy")

controller = ThreadpoolController()
libs = controller.select(user_api="blas").lib_controllers
if not libs:
pytest.skip("No libraries loaded")

@_CDLL_CACHE.cache_method_on_dynlib
def my_extra_method(self):
return object()

# Can't use pytest's monkeypatch since this method doesn't already exist,
# so add it manually:
libs[0].__class__.my_extra_method = my_extra_method

def cleanup():
del libs[0].__class__.my_extra_method

request.addfinalizer(cleanup)

# Accessing underlying, uncached method returns different objects each time:
initial = libs[0].my_extra_method()
assert libs[0].my_extra_method.__wrapped__(libs[0]) is not initial
assert libs[0].my_extra_method.__wrapped__(libs[0]) is not initial

# Only one call should ever happen when using the cached method, however:
assert libs[0].my_extra_method() is initial
assert libs[0].my_extra_method() is initial
Loading
Loading