Skip to content
This repository was archived by the owner on Jan 30, 2023. It is now read-only.

Commit d50f9a6

Browse files
committed
Make "with lazyimport" context more thread-safe
1 parent 8ec9f5d commit d50f9a6

File tree

1 file changed

+90
-4
lines changed

1 file changed

+90
-4
lines changed

src/sage/misc/lazy_import.pyx

+90-4
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,17 @@ cimport cython
6060
from cpython.module cimport PyImport_ImportModuleLevel
6161
from cpython.object cimport PyObject_RichCompare
6262
from cpython.number cimport PyNumber_TrueDivide, PyNumber_Power, PyNumber_Index
63+
from cpython.pystate cimport PyThreadState, PyThreadState_Get
64+
cdef extern from *:
65+
ctypedef PyThreadState volatile_PyThreadState "volatile PyThreadState"
6366

6467
import os
6568
import sys
6669
from six.moves import builtins, cPickle as pickle
6770
import inspect
68-
from . import sageinspect
71+
from time import sleep
6972

73+
from . import sageinspect
7074
from .lazy_import_cache import get_cache_file
7175

7276

@@ -1243,6 +1247,11 @@ cdef class LazyModule:
12431247
return LazyImport(self.module, name=attr, as_name=NotImplemented, **self.kwds)
12441248

12451249

1250+
# Unique thread which is inside a LazyImportContext(). To avoid
1251+
# threading issues, at most one thread can do this. Due to the Python
1252+
# GIL, there are no race conditions in accessing this global pointer.
1253+
cdef volatile_PyThreadState* lazyimport_thread = NULL
1254+
12461255
class LazyImportContext(object):
12471256
"""
12481257
Context in which all imports become lazy imports.
@@ -1327,7 +1336,26 @@ class LazyImportContext(object):
13271336
sage: with lazyimport:
13281337
....: print(__import__)
13291338
<bound method LazyImportContext.__import__ of <sage.misc.lazy_import.LazyImportContext object at ...>>
1330-
"""
1339+
1340+
Nesting is illegal::
1341+
1342+
sage: with lazyimport:
1343+
....: with lazyimport:
1344+
....: pass
1345+
Traceback (most recent call last):
1346+
...
1347+
RuntimeError: nesting "with lazyimport" contexts is not allowed
1348+
"""
1349+
global lazyimport_thread
1350+
while lazyimport_thread is not NULL:
1351+
if lazyimport_thread is PyThreadState_Get():
1352+
# Current thread is already doing lazy imports.
1353+
raise RuntimeError('nesting "with lazyimport" contexts is not allowed')
1354+
# A different thread is doing lazy imports, yield the GIL
1355+
# for 20ms.
1356+
sleep(0.020)
1357+
1358+
lazyimport_thread = PyThreadState_Get()
13311359
self.old_import = builtins.__import__
13321360
builtins.__import__ = self.__import__
13331361
return self
@@ -1351,8 +1379,21 @@ class LazyImportContext(object):
13511379
Exception
13521380
sage: print(__import__)
13531381
<built-in function __import__>
1382+
1383+
We get a message if ``__import__`` was changed inside the
1384+
context::
1385+
1386+
sage: from six.moves import builtins
1387+
sage: with lazyimport:
1388+
....: builtins.__import__ = None
1389+
WARNING: __import__ was changed inside a "with lazyimport" context to None (possibly by a different thread)
13541390
"""
1391+
global lazyimport_thread
1392+
if builtins.__import__ != self.__import__:
1393+
print(f'WARNING: __import__ was changed inside a "with lazyimport" context to {builtins.__import__} (possibly by a different thread)')
13551394
builtins.__import__ = self.old_import
1395+
del self.old_import
1396+
lazyimport_thread = NULL
13561397

13571398
def __call__(self, **kwds):
13581399
"""
@@ -1373,17 +1414,62 @@ class LazyImportContext(object):
13731414
"""
13741415
Replacement function for ``builtins.__import__``.
13751416
1417+
It is not allowed to call this outside of a ``with lazyimport``
1418+
context. If one thread does ``with lazyimport``, then other
1419+
threads doing imports will fall back to the previous (non-lazy)
1420+
``__import__`` function.
1421+
13761422
TESTS::
13771423
13781424
sage: from sage.misc.lazy_import import lazyimport
1379-
sage: lazyimport.__import__("sage.all", {}, {}, ["ZZ"])
1425+
sage: with lazyimport as lazy:
1426+
....: print(lazy.__import__("sage.all", {}, {}, ["ZZ"]))
13801427
<lazy imported module 'sage.all'>
1428+
sage: lazyimport.__import__("sage.all", {}, {}, ["ZZ"])
1429+
Traceback (most recent call last):
1430+
...
1431+
RuntimeError: __import__ called outside "with lazyimport" context
13811432
sage: with lazyimport:
13821433
....: import sage.all
13831434
Traceback (most recent call last):
13841435
...
13851436
RuntimeError: lazy import only works with 'from ... import ...' imports, not with 'import ...'
1386-
"""
1437+
1438+
We test 100 threads each doing an import, half of them lazy and
1439+
half of them a real import. Note that this will not cause a
1440+
lot of CPU activity due to the Python GIL. ::
1441+
1442+
sage: from sage.misc.lazy_import import LazyImport, lazyimport
1443+
sage: from threading import Thread
1444+
sage: from time import sleep
1445+
sage: def do_import():
1446+
....: sleep(0.001) # Yield the GIL
1447+
....: from sage.structure.sage_object import SageObject
1448+
....: t = type(SageObject)
1449+
....: if t is not type:
1450+
....: print("SageObject should be type, got {!r}".format(t))
1451+
sage: def do_lazy_import():
1452+
....: with lazyimport:
1453+
....: sleep(0.001) # Yield the GIL
1454+
....: from sage.structure.sage_object import SageObject
1455+
....: t = type(SageObject)
1456+
....: if t is not LazyImport:
1457+
....: print("SageObject should be LazyImport, got {!r}".format(t))
1458+
sage: threads = [Thread(target=do_import if i%2 else do_lazy_import) for i in range(100)]
1459+
sage: for T in threads:
1460+
....: T.start()
1461+
sage: for T in threads:
1462+
....: T.join()
1463+
"""
1464+
# If __import__ was called from a different thread, call
1465+
# old_import() instead.
1466+
if PyThreadState_Get() is not lazyimport_thread:
1467+
try:
1468+
old = self.old_import
1469+
except AttributeError:
1470+
raise RuntimeError('__import__ called outside "with lazyimport" context')
1471+
return old(module, globals, locals, fromlist, level)
1472+
13871473
if not fromlist:
13881474
raise RuntimeError("lazy import only works with 'from ... import ...' imports, not with 'import ...'")
13891475
return LazyModule(module, namespace=globals, level=level, **self.kwds)

0 commit comments

Comments
 (0)