Skip to content

Commit 6b3ed94

Browse files
committed
Port the reification cache to the current resolution code
Lazy import resolution moved to Objects/lazyimportobject.c and no longer holds the global import lock, so store the result with an atomic compare-and-swap: concurrent resolutions of one proxy return the first result that was stored. Clarify the docs and test concurrent resolve().
1 parent 7c7a1b7 commit 6b3ed94

3 files changed

Lines changed: 45 additions & 3 deletions

File tree

‎Doc/library/types.rst‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -358,9 +358,9 @@ Standard names are defined for the following types:
358358

359359
Reify the lazy import and return the "real" object being imported.
360360

361-
A lazy import is successfully reified at most once. Repeated calls, and any later
362-
access to a name still bound to the proxy, return the object from the
363-
first reification, even if the module has since been removed from
361+
Once a lazy import has been reified successfully, repeated calls, and
362+
any later access to a name still bound to the proxy, return the same
363+
object, even if the module has since been removed from
364364
:data:`sys.modules`. A reification that raises is not remembered, so
365365
the next access retries the import.
366366

‎Lib/test/test_lazy_import/__init__.py‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import contextlib
1414

1515
from test import support
16+
from test.support import threading_helper
1617
from test.support.script_helper import assert_python_ok, assert_python_failure
1718

1819
try:
@@ -365,6 +366,29 @@ def main():
365366
main()
366367
""")
367368

369+
@threading_helper.requires_working_threading()
370+
def test_concurrent_resolve_returns_one_object(self):
371+
"""Concurrent reifications of one proxy all return the same object."""
372+
import builtins
373+
ns = {"__builtins__": {
374+
"__lazy_import__": builtins.__lazy_import__,
375+
"__import__": lambda name, *args: types.ModuleType(name),
376+
}}
377+
exec("lazy import target_module", ns)
378+
proxy = ns["target_module"]
379+
barrier = threading.Barrier(8)
380+
results = []
381+
382+
def worker():
383+
barrier.wait()
384+
results.append(proxy.resolve())
385+
386+
with threading_helper.start_threads(
387+
threading.Thread(target=worker) for _ in range(8)):
388+
pass
389+
self.assertEqual(len(results), 8)
390+
self.assertTrue(all(r is results[0] for r in results))
391+
368392
@support.requires_subprocess()
369393
def test_from_import_proxy_remembers_the_attribute(self):
370394
"""A `lazy from` proxy binds the attribute, not the module."""

‎Objects/lazyimportobject.c‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ typedef struct {
2323
// in lz_from, and the attribute to import from (PyUnicode) it in lz_attr.
2424
PyObject *lz_from;
2525
PyObject *lz_attr;
26+
// Result of the first successful resolution, or NULL.
27+
PyObject *lz_resolved;
2628
// Declaration location.
2729
PyCodeObject *lz_code;
2830
int lz_instr_offset;
@@ -70,6 +72,7 @@ _PyLazyImport_New(_PyInterpreterFrame *frame, PyObject *builtins,
7072
m->lz_builtins = Py_XNewRef(builtins);
7173
m->lz_from = Py_NewRef(name);
7274
m->lz_attr = Py_XNewRef(fromlist);
75+
m->lz_resolved = NULL;
7376

7477
m->lz_code = NULL;
7578
m->lz_instr_offset = -1;
@@ -156,6 +159,7 @@ lazy_import_traverse(PyObject *op, visitproc visit, void *arg)
156159
Py_VISIT(m->lz_builtins);
157160
Py_VISIT(m->lz_from);
158161
Py_VISIT(m->lz_attr);
162+
Py_VISIT(m->lz_resolved);
159163
Py_VISIT(m->lz_code);
160164
return 0;
161165
}
@@ -167,6 +171,7 @@ lazy_import_clear(PyObject *op)
167171
Py_CLEAR(m->lz_builtins);
168172
Py_CLEAR(m->lz_from);
169173
Py_CLEAR(m->lz_attr);
174+
Py_CLEAR(m->lz_resolved);
170175
Py_CLEAR(m->lz_code);
171176
return 0;
172177
}
@@ -368,6 +373,10 @@ lazy_import_resolve_impl(PyThreadState *tstate, PyObject *lazy_import,
368373
assert(PyLazyImport_CheckExact(lazy_import));
369374

370375
PyLazyImportObject *lz = (PyLazyImportObject *)lazy_import;
376+
PyObject *resolved = FT_ATOMIC_LOAD_PTR_ACQUIRE(lz->lz_resolved);
377+
if (resolved != NULL) {
378+
return Py_NewRef(resolved);
379+
}
371380

372381
// Walk back to the placeholder IMPORT_NAME left, and the first lookup on it.
373382
PyLazyImportObject *root = lz, *first = NULL;
@@ -535,6 +544,15 @@ lazy_import_resolve_impl(PyThreadState *tstate, PyObject *lazy_import,
535544
}
536545
Py_XDECREF(name);
537546
}
547+
if (obj != NULL) {
548+
// Concurrent resolutions return the first result that was stored.
549+
PyObject *expected = NULL;
550+
if (!_Py_atomic_compare_exchange_ptr(&lz->lz_resolved, &expected,
551+
Py_NewRef(obj))) {
552+
Py_DECREF(obj);
553+
Py_SETREF(obj, Py_NewRef(expected));
554+
}
555+
}
538556
if (resolving != NULL) {
539557
// A failed set resize can leave the placeholder inserted. Removing by
540558
// identity also permits greenlets to finish in a different order.

0 commit comments

Comments
 (0)