diff --git a/Include/pystate.h b/Include/pystate.h --- a/Include/pystate.h +++ b/Include/pystate.h @@ -113,6 +113,7 @@ typedef struct _ts { PyObject *async_exc; /* Asynchronous exception to raise */ long thread_id; /* Thread id where this tstate was created */ + PyObject *importing; /* A list of modules (full names) being imported */ /* XXX signal handlers should also be here */ } PyThreadState; diff --git a/Python/import.c b/Python/import.c --- a/Python/import.c +++ b/Python/import.c @@ -25,6 +25,368 @@ typedef unsigned short mode_t; #include #endif +/* Locking primitives to prevent parallel imports of the same module + in different threads to return with a partially loaded module. + These calls are serialized by the global interpreter lock. */ + +#ifdef WITH_THREAD +#include "pythread.h" +#endif + +struct importlock { +#ifdef WITH_THREAD + PyThread_type_lock lock; + PyThreadState *tstate; + int waiters; +#endif + int level; +}; + +static int +init_import_lock(struct importlock *lock) +{ +#ifdef WITH_THREAD + lock->lock = PyThread_allocate_lock(); + if (lock->lock == NULL) + return 0; /* Nothing much we can do. */ + lock->tstate = NULL; + lock->waiters = 0; +#endif + lock->level = 0; + return 1; +} + +static void +clear_import_lock(struct importlock *lock) +{ +#ifdef WITH_THREAD + if (lock->lock) { + PyThread_free_lock(lock->lock); + lock->lock = NULL; + } +#endif +} + +static void +acquire_import_lock(struct importlock *lock) +{ +#ifdef WITH_THREAD + PyThreadState *tstate = PyThreadState_GET(); + assert(tstate != NULL); + if (lock->tstate == tstate) { + lock->level++; + return; + } + if (lock->tstate != NULL || !PyThread_acquire_lock(lock->lock, 0)) + { + PyThreadState *saved; + lock->waiters++; + saved = PyEval_SaveThread(); + PyThread_acquire_lock(lock->lock, 1); + PyEval_RestoreThread(saved); + lock->waiters--; + } + assert(lock->level == 0); + lock->tstate = tstate; + lock->level = 1; +#else + lock->level++; +#endif +} + +/* 1 on success, 0 on failure (unacquired lock) */ + +static int +release_import_lock(struct importlock *lock) +{ +#ifdef WITH_THREAD + PyThreadState *tstate = PyThreadState_GET(); + assert(tstate != NULL); + if (lock->lock == NULL) + return 0; /* Too bad */ + if (lock->tstate != tstate) + return 0; + lock->level--; + assert(lock->level >= 0); + if (lock->level == 0) { + lock->tstate = NULL; + PyThread_release_lock(lock->lock); + } + return 1; +#else + if (lock->level <= 0) + return 0; /* Too bad */ + lock->level--; + return 1; +#endif +} + +/* Module-specific import locks */ + +/* Dict of module name -> lock */ +static PyObject *module_locks = NULL; +#define IMPORT_LOCK_CAPSULE_NAME "import.c module lock" + +static int +init_module_locks(void) +{ + Py_CLEAR(module_locks); + module_locks = PyDict_New(); + return module_locks != NULL; +} + +static void +reinit_module_locks(void) +{ + PyObject *key, *capsule; + Py_ssize_t pos = 0; + struct importlock *lock; + int fix_level; + PyThreadState *tstate = PyThreadState_GET(); + + /* We can't remove dict entries while iterating: just keep it simple */ + while (PyDict_Next(module_locks, &pos, &key, &capsule)) { + lock = PyCapsule_GetPointer(capsule, + IMPORT_LOCK_CAPSULE_NAME); + assert(lock != NULL); + fix_level = (lock->tstate == tstate) ? lock->level : 0; + init_import_lock(lock); + if (fix_level > 0) { + acquire_import_lock(lock); + lock->level = fix_level; + } + } +} + +/* + Returns the lock structure for the given module name, or NULL if not + found. + (note: NULL with an exception means another error occurred). +*/ +static struct importlock * +_get_module_lock(PyObject *fullname) +{ + PyObject *capsule; + struct importlock *lock; + if (module_locks == NULL) + return NULL; + capsule = PyDict_GetItemWithError(module_locks, fullname); + if (capsule == NULL) + return NULL; + lock = PyCapsule_GetPointer(capsule, + IMPORT_LOCK_CAPSULE_NAME); + assert(lock != NULL); + return lock; +} + +/* + Release the lock structure for the given module name, destroying it + if possible. +*/ +static int +_release_module_lock(PyObject *fullname) +{ + PyObject *capsule; + struct importlock *lock; + int r; + if (module_locks == NULL) + return 0; + capsule = PyDict_GetItemWithError(module_locks, fullname); + if (capsule == NULL) + return -1; + lock = PyCapsule_GetPointer(capsule, + IMPORT_LOCK_CAPSULE_NAME); + assert(lock != NULL); + Py_INCREF(capsule); + if (lock->level == 1 && lock->waiters == 0) { + r = PyDict_DelItem(module_locks, fullname); + if (r) + return r; + } + r = release_import_lock(lock); + Py_DECREF(capsule); + return r; +} + +static void +_destroy_module_lock(PyObject *capsule) +{ + struct importlock *lock = PyCapsule_GetPointer(capsule, + IMPORT_LOCK_CAPSULE_NAME); + assert(lock != NULL); + clear_import_lock(lock); + PyObject_FREE(lock); +} + +/* NULL if an exception occurred. */ +static struct importlock * +_create_module_lock(PyObject *fullname) +{ + PyObject *capsule; + struct importlock *lock; + int r; + if (module_locks == NULL && !init_module_locks()) + return NULL; + lock = (struct importlock *) PyObject_MALLOC(sizeof(struct importlock)); + if (lock == NULL) + return NULL; + capsule = PyCapsule_New(lock, IMPORT_LOCK_CAPSULE_NAME, _destroy_module_lock); + if (capsule == NULL) { + PyObject_Free(lock); + return NULL; + } + if (!init_import_lock(lock)) { + PyErr_Format(PyExc_RuntimeError, + "Couldn't initialize import lock for %R", + fullname); + Py_DECREF(capsule); + return NULL; + } + r = PyDict_SetItem(module_locks, fullname, capsule); + Py_DECREF(capsule); + if (r < 0) + return NULL; + return lock; +} + +static int +detect_circularity(PyThreadState *tstate, struct importlock *lock) +{ + PyThreadState *other = tstate; + Py_ssize_t n; + PyObject *fullname; + + while (1) { + if (lock->tstate == NULL || lock->tstate == other) + /* Lock can be taken without blocking */ + return 0; + if (lock->tstate == tstate) { + fprintf(stderr, "!! circularity detected through %s\n", PyUnicode_AsUTF8(fullname)); + return 1; + } + other = lock->tstate; + /* What lock is the thread currently trying to take? */ + assert(other->importing != NULL); + n = PyList_GET_SIZE(other->importing); + assert(n > 0); + fullname = PyList_GET_ITEM(other->importing, n - 1); + lock = _get_module_lock(fullname); + assert(lock != NULL); + } + return 0; +} + +/* 1 on success, 0 on deadlock (threaded circular import), -1 on exception */ + +static int +acquire_module_lock(PyObject *fullname) +{ + struct importlock *lock; +#ifdef WITH_THREAD + PyThreadState *tstate = PyThreadState_GET(); + assert(tstate != NULL); +#endif + assert(PyUnicode_Check(fullname)); + _PyImport_AcquireLock(); + lock = _get_module_lock(fullname); + if (!lock && !PyErr_Occurred()) + lock = _create_module_lock(fullname); + if (!lock) { + _PyImport_ReleaseLock(); + return -1; + } +#ifdef WITH_THREAD + if (tstate->importing == NULL) { + tstate->importing = PyList_New(0); + if (tstate->importing == NULL) { + _PyImport_ReleaseLock(); + return -1; + } + } + if (detect_circularity(tstate, lock)) { + _PyImport_ReleaseLock(); + return 0; + } + /* Register the current thread as importing `fullname` */ + if (PyList_Append(tstate->importing, fullname)) { + _PyImport_ReleaseLock(); + return -1; + } +#endif + _PyImport_ReleaseLock(); + acquire_import_lock(lock); + return 1; +} + +/* 1 on success, 0 on failure (unacquired lock), -1 on exception */ + +static int +release_module_lock(PyObject *fullname) +{ + int r; + r = _release_module_lock(fullname); + if (r == 1) { +#ifdef WITH_THREAD + Py_ssize_t n; + PyThreadState *tstate = PyThreadState_GET(); + assert(tstate != NULL); + assert(tstate->importing != NULL); + n = PyList_GET_SIZE(tstate->importing); + assert(n > 0); + if (PyList_SetSlice(tstate->importing, n - 1, n, NULL)) + return -1; +#endif + } + return r; +} + + +/* XXX we could define them for non-threaded builds too */ +#ifdef WITH_THREAD + +static struct importlock global_import_lock; + +void +_PyImport_AcquireLock(void) +{ + struct importlock *lock = &global_import_lock; + /* This function can be called before _PyImport_Init(), therefore + we have to init the import lock ourselves */ + if (lock->lock == NULL) { + if (init_import_lock(lock) <= 0) + Py_FatalError("Failed to initialized the import lock"); + } + acquire_import_lock(lock); +} + +int +_PyImport_ReleaseLock(void) +{ + struct importlock *lock = &global_import_lock; + return release_import_lock(lock) > 0 ? 1 : -1; +} + +/* This function is called from PyOS_AfterFork to ensure that newly + created child processes do not share locks with the parent. + We now acquire the import lock around fork() calls but on some platforms + (Solaris 9 and earlier? see isue7242) that still left us with problems. */ + +void +_PyImport_ReInitLock(void) +{ + int old_level = global_import_lock.level; + init_import_lock(&global_import_lock); + if (old_level > 1) { + /* Forked as a side effect of import */ + acquire_import_lock(&global_import_lock); + global_import_lock.level = old_level; + } + reinit_module_locks(); +} + +#endif + + /* Magic word to reject .pyc files generated by other Python versions. It should change for each incompatible change to the bytecode. @@ -162,6 +524,12 @@ void struct filedescr *filetab; int countD = 0; int countS = 0; + struct importlock *lock = &global_import_lock; + + if (lock->lock == NULL && lock->tstate == NULL) { + if (init_import_lock(&global_import_lock) <= 0) + Py_FatalError("Failed to initialized the import lock"); + } initstr = PyUnicode_InternFromString("__init__"); if (initstr == NULL) @@ -269,89 +637,13 @@ void Py_DECREF(path_hooks); } -/* Locking primitives to prevent parallel imports of the same module - in different threads to return with a partially loaded module. - These calls are serialized by the global interpreter lock. */ - -#ifdef WITH_THREAD - -#include "pythread.h" - -static PyThread_type_lock import_lock = 0; -static long import_lock_thread = -1; -static int import_lock_level = 0; - -void -_PyImport_AcquireLock(void) -{ - long me = PyThread_get_thread_ident(); - if (me == -1) - return; /* Too bad */ - if (import_lock == NULL) { - import_lock = PyThread_allocate_lock(); - if (import_lock == NULL) - return; /* Nothing much we can do. */ - } - if (import_lock_thread == me) { - import_lock_level++; - return; - } - if (import_lock_thread != -1 || !PyThread_acquire_lock(import_lock, 0)) - { - PyThreadState *tstate = PyEval_SaveThread(); - PyThread_acquire_lock(import_lock, 1); - PyEval_RestoreThread(tstate); - } - import_lock_thread = me; - import_lock_level = 1; -} - -int -_PyImport_ReleaseLock(void) -{ - long me = PyThread_get_thread_ident(); - if (me == -1 || import_lock == NULL) - return 0; /* Too bad */ - if (import_lock_thread != me) - return -1; - import_lock_level--; - if (import_lock_level == 0) { - import_lock_thread = -1; - PyThread_release_lock(import_lock); - } - return 1; -} - -/* This function is called from PyOS_AfterFork to ensure that newly - created child processes do not share locks with the parent. - We now acquire the import lock around fork() calls but on some platforms - (Solaris 9 and earlier? see isue7242) that still left us with problems. */ - -void -_PyImport_ReInitLock(void) -{ - if (import_lock != NULL) - import_lock = PyThread_allocate_lock(); - if (import_lock_level > 1) { - /* Forked as a side effect of import */ - long me = PyThread_get_thread_ident(); - PyThread_acquire_lock(import_lock, 0); - /* XXX: can the previous line fail? */ - import_lock_thread = me; - import_lock_level--; - } else { - import_lock_thread = -1; - import_lock_level = 0; - } -} - -#endif +/* Methods of the `imp` module */ static PyObject * imp_lock_held(PyObject *self, PyObject *noargs) { #ifdef WITH_THREAD - return PyBool_FromLong(import_lock_thread != -1); + return PyBool_FromLong(global_import_lock.tstate != NULL); #else return PyBool_FromLong(0); #endif @@ -388,12 +680,7 @@ void extensions = NULL; PyMem_DEL(_PyImport_Filetab); _PyImport_Filetab = NULL; -#ifdef WITH_THREAD - if (import_lock != NULL) { - PyThread_free_lock(import_lock); - import_lock = NULL; - } -#endif + clear_import_lock(&global_import_lock); } static void @@ -2498,7 +2785,11 @@ load_module(PyObject *name, FILE *fp, Py "import hook without loader"); return NULL; } + /* For backwards compatibility with non-thread safe import hooks. + XXX perhaps we should allow hooks to choose this */ + _PyImport_AcquireLock(); m = _PyObject_CallMethodId(loader, &PyId_load_module, "O", name); + _PyImport_ReleaseLock(); break; } @@ -2729,14 +3020,15 @@ PyImport_ImportModule(const char *name) * ImportError instead of blocking. * * Returns the module object with incremented ref count. + * + * XXX this function should be modified in regard of the new module-specific + * locks. */ PyObject * PyImport_ImportModuleNoBlock(const char *name) { PyObject *nameobj, *modules, *result; -#ifdef WITH_THREAD - long me; -#endif + struct importlock *lock = &global_import_lock; /* Try to get the module from sys.modules[name] */ modules = PyImport_GetModuleDict(); @@ -2757,8 +3049,7 @@ PyImport_ImportModuleNoBlock(const char /* check the import lock * me might be -1 but I ignore the error here, the lock function * takes care of the problem */ - me = PyThread_get_thread_ident(); - if (import_lock_thread == -1 || import_lock_thread == me) { + if (lock->tstate == NULL || lock->tstate == PyThreadState_GET()) { /* no thread or me is holding the lock */ result = PyImport_Import(nameobj); } @@ -2891,14 +3182,7 @@ PyImport_ImportModuleLevelObject(PyObjec int level) { PyObject *mod; - _PyImport_AcquireLock(); mod = import_module_level(name, globals, locals, fromlist, level); - if (_PyImport_ReleaseLock() < 0) { - Py_XDECREF(mod); - PyErr_SetString(PyExc_RuntimeError, - "not holding the import lock"); - return NULL; - } return mod; } @@ -3294,16 +3578,22 @@ import_submodule(PyObject *mod, PyObject PyObject *m = NULL, *bufobj, *path_list, *loader; struct filedescr *fdp; FILE *fp; + int r, is_circular; /* Require: if mod == None: subname == fullname else: mod.__name__ + "." + subname == fullname */ + r = acquire_module_lock(fullname); + if (r < 0) + return NULL; + is_circular = (r == 0); if ((m = PyDict_GetItem(modules, fullname)) != NULL) { Py_INCREF(m); - return m; + goto bail; } + assert(r > 0); if (mod == Py_None) path_list = NULL; @@ -3311,8 +3601,9 @@ import_submodule(PyObject *mod, PyObject path_list = _PyObject_GetAttrId(mod, &PyId___path__); if (path_list == NULL) { PyErr_Clear(); + m = Py_None; Py_INCREF(Py_None); - return Py_None; + goto bail; } } @@ -3321,10 +3612,11 @@ import_submodule(PyObject *mod, PyObject Py_XDECREF(path_list); if (fdp == NULL) { if (!PyErr_ExceptionMatches(PyExc_ImportError)) - return NULL; + goto bail; PyErr_Clear(); + m = Py_None; Py_INCREF(Py_None); - return Py_None; + goto bail; } m = load_module(fullname, fp, bufobj, fdp->type, loader); Py_XDECREF(bufobj); @@ -3332,11 +3624,13 @@ import_submodule(PyObject *mod, PyObject if (fp) fclose(fp); if (m == NULL) - return NULL; - if (!add_submodule(mod, m, fullname, subname, modules)) { - Py_XDECREF(m); - return NULL; - } + goto bail; + if (!add_submodule(mod, m, fullname, subname, modules)) + Py_CLEAR(m); + +bail: + if (!is_circular) + release_module_lock(fullname); return m; } @@ -3427,14 +3721,14 @@ PyImport_ReloadModule(PyObject *m) Py_DECREF(subname); Py_XDECREF(path_list); - if (fdp == NULL) { - Py_XDECREF(loader); + if (fdp == NULL) goto error; - } - + + if (acquire_module_lock(name) < 0) + goto error; newm = load_module(name, fp, bufobj, fdp->type, loader); - Py_XDECREF(bufobj); - Py_XDECREF(loader); + if (release_module_lock(name) < 0) + goto error; if (fp) fclose(fp); @@ -3449,6 +3743,8 @@ PyImport_ReloadModule(PyObject *m) error: imp_modules_reloading_clear(); + Py_XDECREF(bufobj); + Py_XDECREF(loader); Py_DECREF(name); return newm; } diff --git a/Python/pystate.c b/Python/pystate.c --- a/Python/pystate.c +++ b/Python/pystate.c @@ -191,6 +191,7 @@ new_threadstate(PyInterpreterState *inte #else tstate->thread_id = 0; #endif + tstate->importing = NULL; tstate->dict = NULL; @@ -285,6 +286,7 @@ PyThreadState_Clear(PyThreadState *tstat Py_CLEAR(tstate->dict); Py_CLEAR(tstate->async_exc); + Py_CLEAR(tstate->importing); Py_CLEAR(tstate->curexc_type); Py_CLEAR(tstate->curexc_value);