diff --git a/numpy/_core/meson.build b/numpy/_core/meson.build index 78b60364bea5..90f29a0f6d7f 100644 --- a/numpy/_core/meson.build +++ b/numpy/_core/meson.build @@ -1041,6 +1041,7 @@ src_multiarray_umath_common = [ 'src/common/mem_overlap.c', 'src/common/npy_argparse.c', 'src/common/npy_hashtable.c', + 'src/common/npy_import.c', 'src/common/npy_longdouble.c', 'src/common/ucsnarrow.c', 'src/common/ufunc_override.c', diff --git a/numpy/_core/src/common/npy_argparse.c b/numpy/_core/src/common/npy_argparse.c index 2be17483ec28..eb1597c0ebb9 100644 --- a/numpy/_core/src/common/npy_argparse.c +++ b/numpy/_core/src/common/npy_argparse.c @@ -7,11 +7,22 @@ #include "numpy/ndarraytypes.h" #include "numpy/npy_2_compat.h" #include "npy_argparse.h" - +#include "npy_atomic.h" #include "npy_import.h" #include "arrayfunction_override.h" +static PyThread_type_lock argparse_mutex; + +NPY_NO_EXPORT int +init_argparse_mutex(void) { + argparse_mutex = PyThread_allocate_lock(); + if (argparse_mutex == NULL) { + PyErr_NoMemory(); + return -1; + } + return 0; +} /** * Small wrapper converting to array just like CPython does. @@ -274,15 +285,20 @@ _npy_parse_arguments(const char *funcname, /* ... is NULL, NULL, NULL terminated: name, converter, value */ ...) { - if (NPY_UNLIKELY(cache->npositional == -1)) { - va_list va; - va_start(va, kwnames); - - int res = initialize_keywords(funcname, cache, va); - va_end(va); - if (res < 0) { - return -1; + if (!npy_atomic_load_uint8(&cache->initialized)) { + PyThread_acquire_lock(argparse_mutex, WAIT_LOCK); + if (!npy_atomic_load_uint8(&cache->initialized)) { + va_list va; + va_start(va, kwnames); + int res = initialize_keywords(funcname, cache, va); + va_end(va); + if (res < 0) { + PyThread_release_lock(argparse_mutex); + return -1; + } + npy_atomic_store_uint8(&cache->initialized, 1); } + PyThread_release_lock(argparse_mutex); } if (NPY_UNLIKELY(len_args > cache->npositional)) { diff --git a/numpy/_core/src/common/npy_argparse.h b/numpy/_core/src/common/npy_argparse.h index f4122103d22b..9f69da1307b5 100644 --- a/numpy/_core/src/common/npy_argparse.h +++ b/numpy/_core/src/common/npy_argparse.h @@ -20,7 +20,6 @@ NPY_NO_EXPORT int PyArray_PythonPyIntFromInt(PyObject *obj, int *value); - #define _NPY_MAX_KWARGS 15 typedef struct { @@ -28,16 +27,18 @@ typedef struct { int nargs; int npositional_only; int nrequired; + npy_uint8 initialized; /* Null terminated list of keyword argument name strings */ PyObject *kw_strings[_NPY_MAX_KWARGS+1]; } _NpyArgParserCache; +NPY_NO_EXPORT int init_argparse_mutex(void); /* * The sole purpose of this macro is to hide the argument parsing cache. * Since this cache must be static, this also removes a source of error. */ -#define NPY_PREPARE_ARGPARSER static _NpyArgParserCache __argparse_cache = {-1} +#define NPY_PREPARE_ARGPARSER static _NpyArgParserCache __argparse_cache; /** * Macro to help with argument parsing. diff --git a/numpy/_core/src/common/npy_atomic.h b/numpy/_core/src/common/npy_atomic.h new file mode 100644 index 000000000000..b92d58d583c0 --- /dev/null +++ b/numpy/_core/src/common/npy_atomic.h @@ -0,0 +1,99 @@ +/* + * Provides wrappers around C11 standard library atomics and MSVC intrinsics + * to provide basic atomic load and store functionality. This is based on + * code in CPython's pyatomic.h, pyatomic_std.h, and pyatomic_msc.h + */ + +#ifndef NUMPY_CORE_SRC_COMMON_NPY_ATOMIC_H_ +#define NUMPY_CORE_SRC_COMMON_NPY_ATOMIC_H_ + +#include "numpy/npy_common.h" + +#if __STDC_VERSION__ >= 201112L && !defined(__STDC_NO_ATOMICS__) +// TODO: support C++ atomics as well if this header is ever needed in C++ + #include + #include + #define STDC_ATOMICS +#elif _MSC_VER + #include + #define MSC_ATOMICS + #if !defined(_M_X64) && !defined(_M_IX86) && !defined(_M_ARM64) + #error "Unsupported MSVC build configuration, neither x86 or ARM" + #endif +#elif defined(__GNUC__) && (__GNUC__ > 4) + #define GCC_ATOMICS +#elif defined(__clang__) + #if __has_builtin(__atomic_load) + #define GCC_ATOMICS + #endif +#else + #error "no supported atomic implementation for this platform/compiler" +#endif + + +static inline npy_uint8 +npy_atomic_load_uint8(const npy_uint8 *obj) { +#ifdef STDC_ATOMICS + return (npy_uint8)atomic_load((const _Atomic(uint8_t)*)obj); +#elif defined(MSC_ATOMICS) +#if defined(_M_X64) || defined(_M_IX86) + return *(volatile npy_uint8 *)obj; +#else // defined(_M_ARM64) + return (npy_uint8)__ldar8((unsigned __int8 volatile *)obj); +#endif +#elif defined(GCC_ATOMICS) + return __atomic_load_n(obj, __ATOMIC_SEQ_CST); +#endif +} + +static inline void* +npy_atomic_load_ptr(const void *obj) { +#ifdef STDC_ATOMICS + return atomic_load((const _Atomic(void *)*)obj); +#elif defined(MSC_ATOMICS) +#if SIZEOF_VOID_P == 8 +#if defined(_M_X64) || defined(_M_IX86) + return *(volatile uint64_t *)obj; +#elif defined(_M_ARM64) + return (uint64_t)__ldar64((unsigned __int64 volatile *)obj); +#endif +#else +#if defined(_M_X64) || defined(_M_IX86) + return *(volatile uint32_t *)obj; +#elif defined(_M_ARM64) + return (uint32_t)__ldar32((unsigned __int32 volatile *)obj); +#endif +#endif +#elif defined(GCC_ATOMICS) + return (void *)__atomic_load_n((void * const *)obj, __ATOMIC_SEQ_CST); +#endif +} + +static inline void +npy_atomic_store_uint8(npy_uint8 *obj, npy_uint8 value) { +#ifdef STDC_ATOMICS + atomic_store((_Atomic(uint8_t)*)obj, value); +#elif defined(MSC_ATOMICS) + _InterlockedExchange8((volatile char *)obj, (char)value); +#elif defined(GCC_ATOMICS) + __atomic_store_n(obj, value, __ATOMIC_SEQ_CST); +#endif +} + +static inline void +npy_atomic_store_ptr(void *obj, void *value) +{ +#ifdef STDC_ATOMICS + atomic_store((_Atomic(void *)*)obj, value); +#elif defined(MSC_ATOMICS) + _InterlockedExchangePointer((void * volatile *)obj, (void *)value); +#elif defined(GCC_ATOMICS) + __atomic_store_n((void **)obj, value, __ATOMIC_SEQ_CST); +#endif +} + +#undef MSC_ATOMICS +#undef STDC_ATOMICS +#undef GCC_ATOMICS + +#endif // NUMPY_CORE_SRC_COMMON_NPY_NPY_ATOMIC_H_ diff --git a/numpy/_core/src/common/npy_ctypes.h b/numpy/_core/src/common/npy_ctypes.h index c72d2dff7fcb..78809732416c 100644 --- a/numpy/_core/src/common/npy_ctypes.h +++ b/numpy/_core/src/common/npy_ctypes.h @@ -21,14 +21,15 @@ npy_ctypes_check(PyTypeObject *obj) PyObject *ret_obj; int ret; - npy_cache_import("numpy._core._internal", "npy_ctypes_check", - &npy_thread_unsafe_state.npy_ctypes_check); - if (npy_thread_unsafe_state.npy_ctypes_check == NULL) { + + if (npy_cache_import_runtime( + "numpy._core._internal", "npy_ctypes_check", + &npy_runtime_imports.npy_ctypes_check) == -1) { goto fail; } - ret_obj = PyObject_CallFunctionObjArgs(npy_thread_unsafe_state.npy_ctypes_check, - (PyObject *)obj, NULL); + ret_obj = PyObject_CallFunctionObjArgs( + npy_runtime_imports.npy_ctypes_check, (PyObject *)obj, NULL); if (ret_obj == NULL) { goto fail; } diff --git a/numpy/_core/src/common/npy_import.c b/numpy/_core/src/common/npy_import.c new file mode 100644 index 000000000000..d220b840f7a9 --- /dev/null +++ b/numpy/_core/src/common/npy_import.c @@ -0,0 +1,19 @@ +#define NPY_NO_DEPRECATED_API NPY_API_VERSION +#define _MULTIARRAYMODULE + +#include "numpy/ndarraytypes.h" +#include "npy_import.h" +#include "npy_atomic.h" + + +NPY_VISIBILITY_HIDDEN npy_runtime_imports_struct npy_runtime_imports; + +NPY_NO_EXPORT int +init_import_mutex(void) { + npy_runtime_imports.import_mutex = PyThread_allocate_lock(); + if (npy_runtime_imports.import_mutex == NULL) { + PyErr_NoMemory(); + return -1; + } + return 0; +} diff --git a/numpy/_core/src/common/npy_import.h b/numpy/_core/src/common/npy_import.h index 58b4ba0bc7e5..89b300159b61 100644 --- a/numpy/_core/src/common/npy_import.h +++ b/numpy/_core/src/common/npy_import.h @@ -3,7 +3,70 @@ #include -/*! \brief Fetch and cache Python function. +#include "numpy/npy_common.h" +#include "npy_atomic.h" + +/* + * Cached references to objects obtained via an import. All of these are + * can be initialized at any time by npy_cache_import_runtime. + */ +typedef struct npy_runtime_imports_struct { + PyThread_type_lock import_mutex; + PyObject *_add_dtype_helper; + PyObject *_all; + PyObject *_amax; + PyObject *_amin; + PyObject *_any; + PyObject *array_function_errmsg_formatter; + PyObject *array_ufunc_errmsg_formatter; + PyObject *_clip; + PyObject *_commastring; + PyObject *_convert_to_stringdtype_kwargs; + PyObject *_default_array_repr; + PyObject *_default_array_str; + PyObject *_dump; + PyObject *_dumps; + PyObject *_getfield_is_safe; + PyObject *internal_gcd_func; + PyObject *_mean; + PyObject *NO_NEP50_WARNING; + PyObject *npy_ctypes_check; + PyObject *numpy_matrix; + PyObject *_prod; + PyObject *_promote_fields; + PyObject *_std; + PyObject *_sum; + PyObject *_ufunc_doc_signature_formatter; + PyObject *_var; + PyObject *_view_is_safe; + PyObject *_void_scalar_to_string; +} npy_runtime_imports_struct; + +NPY_VISIBILITY_HIDDEN extern npy_runtime_imports_struct npy_runtime_imports; + +/*! \brief Import a Python object. + + * This function imports the Python function specified by + * \a module and \a function, increments its reference count, and returns + * the result. On error, returns NULL. + * + * @param module Absolute module name. + * @param attr module attribute to cache. + */ +static inline PyObject* +npy_import(const char *module, const char *attr) +{ + PyObject *ret = NULL; + PyObject *mod = PyImport_ImportModule(module); + + if (mod != NULL) { + ret = PyObject_GetAttrString(mod, attr); + Py_DECREF(mod); + } + return ret; +} + +/*! \brief Fetch and cache Python object at runtime. * * Import a Python function and cache it for use. The function checks if * cache is NULL, and if not NULL imports the Python function specified by @@ -16,17 +79,24 @@ * @param attr module attribute to cache. * @param cache Storage location for imported function. */ -static inline void -npy_cache_import(const char *module, const char *attr, PyObject **cache) -{ - if (NPY_UNLIKELY(*cache == NULL)) { - PyObject *mod = PyImport_ImportModule(module); - - if (mod != NULL) { - *cache = PyObject_GetAttrString(mod, attr); - Py_DECREF(mod); +static inline int +npy_cache_import_runtime(const char *module, const char *attr, PyObject **obj) { + if (!npy_atomic_load_ptr(obj)) { + PyObject* value = npy_import(module, attr); + if (value == NULL) { + return -1; } + PyThread_acquire_lock(npy_runtime_imports.import_mutex, WAIT_LOCK); + if (!npy_atomic_load_ptr(obj)) { + npy_atomic_store_ptr(obj, Py_NewRef(value)); + } + PyThread_release_lock(npy_runtime_imports.import_mutex); + Py_DECREF(value); } + return 0; } +NPY_NO_EXPORT int +init_import_mutex(void); + #endif /* NUMPY_CORE_SRC_COMMON_NPY_IMPORT_H_ */ diff --git a/numpy/_core/src/multiarray/_multiarray_tests.c.src b/numpy/_core/src/multiarray/_multiarray_tests.c.src index fbd5fc445a2c..5dd5057f9602 100644 --- a/numpy/_core/src/multiarray/_multiarray_tests.c.src +++ b/numpy/_core/src/multiarray/_multiarray_tests.c.src @@ -42,6 +42,28 @@ argparse_example_function(PyObject *NPY_UNUSED(mod), Py_RETURN_NONE; } +/* + * Tests that argparse cache creation is thread-safe. *must* be called only + * by the python-level test_thread_safe_argparse_cache function, otherwise + * the cache might be created before the test to make sure cache creation is + * thread-safe runs + */ +static PyObject * +threaded_argparse_example_function(PyObject *NPY_UNUSED(mod), + PyObject *const *args, Py_ssize_t len_args, PyObject *kwnames) +{ + NPY_PREPARE_ARGPARSER; + int arg1; + PyObject *arg2; + if (npy_parse_arguments("thread_func", args, len_args, kwnames, + "$arg1", &PyArray_PythonPyIntFromInt, &arg1, + "$arg2", NULL, &arg2, + NULL, NULL, NULL) < 0) { + return NULL; + } + Py_RETURN_NONE; +} + /* test PyArray_IsPythonScalar, before including private py3 compat header */ static PyObject * IsPythonScalar(PyObject * dummy, PyObject *args) @@ -2205,6 +2227,9 @@ static PyMethodDef Multiarray_TestsMethods[] = { {"argparse_example_function", (PyCFunction)argparse_example_function, METH_KEYWORDS | METH_FASTCALL, NULL}, + {"threaded_argparse_example_function", + (PyCFunction)threaded_argparse_example_function, + METH_KEYWORDS | METH_FASTCALL, NULL}, {"IsPythonScalar", IsPythonScalar, METH_VARARGS, NULL}, @@ -2407,6 +2432,9 @@ PyMODINIT_FUNC PyInit__multiarray_tests(void) return m; } import_array(); + if (init_argparse_mutex() < 0) { + return NULL; + } if (PyErr_Occurred()) { PyErr_SetString(PyExc_RuntimeError, "cannot load _multiarray_tests module."); diff --git a/numpy/_core/src/multiarray/arrayfunction_override.c b/numpy/_core/src/multiarray/arrayfunction_override.c index aa3ab42433c7..e4248ad29aba 100644 --- a/numpy/_core/src/multiarray/arrayfunction_override.c +++ b/numpy/_core/src/multiarray/arrayfunction_override.c @@ -232,12 +232,12 @@ static void set_no_matching_types_error(PyObject *public_api, PyObject *types) { /* No acceptable override found, raise TypeError. */ - npy_cache_import("numpy._core._internal", - "array_function_errmsg_formatter", - &npy_thread_unsafe_state.array_function_errmsg_formatter); - if (npy_thread_unsafe_state.array_function_errmsg_formatter != NULL) { + if (npy_cache_import_runtime( + "numpy._core._internal", + "array_function_errmsg_formatter", + &npy_runtime_imports.array_function_errmsg_formatter) == 0) { PyObject *errmsg = PyObject_CallFunctionObjArgs( - npy_thread_unsafe_state.array_function_errmsg_formatter, + npy_runtime_imports.array_function_errmsg_formatter, public_api, types, NULL); if (errmsg != NULL) { PyErr_SetObject(PyExc_TypeError, errmsg); diff --git a/numpy/_core/src/multiarray/convert_datatype.c b/numpy/_core/src/multiarray/convert_datatype.c index f029ad8a5986..550d3e253868 100644 --- a/numpy/_core/src/multiarray/convert_datatype.c +++ b/numpy/_core/src/multiarray/convert_datatype.c @@ -83,15 +83,14 @@ npy_give_promotion_warnings(void) { PyObject *val; - npy_cache_import( + if (npy_cache_import_runtime( "numpy._core._ufunc_config", "NO_NEP50_WARNING", - &npy_thread_unsafe_state.NO_NEP50_WARNING); - if (npy_thread_unsafe_state.NO_NEP50_WARNING == NULL) { + &npy_runtime_imports.NO_NEP50_WARNING) == -1) { PyErr_WriteUnraisable(NULL); return 1; } - if (PyContextVar_Get(npy_thread_unsafe_state.NO_NEP50_WARNING, + if (PyContextVar_Get(npy_runtime_imports.NO_NEP50_WARNING, Py_False, &val) < 0) { /* Errors should not really happen, but if it does assume we warn. */ PyErr_WriteUnraisable(NULL); diff --git a/numpy/_core/src/multiarray/descriptor.c b/numpy/_core/src/multiarray/descriptor.c index b9d30c80a2f8..a47a71d39196 100644 --- a/numpy/_core/src/multiarray/descriptor.c +++ b/numpy/_core/src/multiarray/descriptor.c @@ -726,12 +726,12 @@ _convert_from_commastring(PyObject *obj, int align) PyObject *parsed; PyArray_Descr *res; assert(PyUnicode_Check(obj)); - npy_cache_import("numpy._core._internal", "_commastring", - &npy_thread_unsafe_state._commastring); - if (npy_thread_unsafe_state._commastring == NULL) { + if (npy_cache_import_runtime( + "numpy._core._internal", "_commastring", + &npy_runtime_imports._commastring) == -1) { return NULL; } - parsed = PyObject_CallOneArg(npy_thread_unsafe_state._commastring, obj); + parsed = PyObject_CallOneArg(npy_runtime_imports._commastring, obj); if (parsed == NULL) { return NULL; } diff --git a/numpy/_core/src/multiarray/dtypemeta.c b/numpy/_core/src/multiarray/dtypemeta.c index a8ba51fa6e06..f46e882ec2d1 100644 --- a/numpy/_core/src/multiarray/dtypemeta.c +++ b/numpy/_core/src/multiarray/dtypemeta.c @@ -766,13 +766,13 @@ void_common_instance(_PyArray_LegacyDescr *descr1, _PyArray_LegacyDescr *descr2) if (descr1->names != NULL && descr2->names != NULL) { /* If both have fields promoting individual fields may be possible */ - npy_cache_import("numpy._core._internal", "_promote_fields", - &npy_thread_unsafe_state._promote_fields); - if (npy_thread_unsafe_state._promote_fields == NULL) { + if (npy_cache_import_runtime( + "numpy._core._internal", "_promote_fields", + &npy_runtime_imports._promote_fields) == -1) { return NULL; } PyObject *result = PyObject_CallFunctionObjArgs( - npy_thread_unsafe_state._promote_fields, + npy_runtime_imports._promote_fields, descr1, descr2, NULL); if (result == NULL) { return NULL; @@ -1240,14 +1240,13 @@ dtypemeta_wrap_legacy_descriptor( /* And it to the types submodule if it is a builtin dtype */ if (!PyTypeNum_ISUSERDEF(descr->type_num)) { - npy_cache_import("numpy.dtypes", "_add_dtype_helper", - &npy_thread_unsafe_state._add_dtype_helper); - if (npy_thread_unsafe_state._add_dtype_helper == NULL) { + if (npy_cache_import_runtime("numpy.dtypes", "_add_dtype_helper", + &npy_runtime_imports._add_dtype_helper) == -1) { return -1; } if (PyObject_CallFunction( - npy_thread_unsafe_state._add_dtype_helper, + npy_runtime_imports._add_dtype_helper, "Os", (PyObject *)dtype_class, alias) == NULL) { return -1; } diff --git a/numpy/_core/src/multiarray/getset.c b/numpy/_core/src/multiarray/getset.c index 092ac65bbbc3..94028f71f964 100644 --- a/numpy/_core/src/multiarray/getset.c +++ b/numpy/_core/src/multiarray/getset.c @@ -388,13 +388,13 @@ array_descr_set(PyArrayObject *self, PyObject *arg, void *NPY_UNUSED(ignored)) if (_may_have_objects(PyArray_DESCR(self)) || _may_have_objects(newtype)) { PyObject *safe; - npy_cache_import("numpy._core._internal", "_view_is_safe", - &npy_thread_unsafe_state._view_is_safe); - if (npy_thread_unsafe_state._view_is_safe == NULL) { + if (npy_cache_import_runtime( + "numpy._core._internal", "_view_is_safe", + &npy_runtime_imports._view_is_safe) == -1) { goto fail; } - safe = PyObject_CallFunction(npy_thread_unsafe_state._view_is_safe, + safe = PyObject_CallFunction(npy_runtime_imports._view_is_safe, "OO", PyArray_DESCR(self), newtype); if (safe == NULL) { goto fail; diff --git a/numpy/_core/src/multiarray/methods.c b/numpy/_core/src/multiarray/methods.c index 669d5e575c7a..70f487393842 100644 --- a/numpy/_core/src/multiarray/methods.c +++ b/numpy/_core/src/multiarray/methods.c @@ -113,13 +113,11 @@ npy_forward_method( * be correct. */ #define NPY_FORWARD_NDARRAY_METHOD(name) \ - npy_cache_import( \ - "numpy._core._methods", #name, \ - &npy_thread_unsafe_state.name); \ - if (npy_thread_unsafe_state.name == NULL) { \ + if (npy_cache_import_runtime("numpy._core._methods", #name, \ + &npy_runtime_imports.name) == -1) { \ return NULL; \ } \ - return npy_forward_method(npy_thread_unsafe_state.name, \ + return npy_forward_method(npy_runtime_imports.name, \ (PyObject *)self, args, len_args, kwnames) @@ -406,15 +404,15 @@ PyArray_GetField(PyArrayObject *self, PyArray_Descr *typed, int offset) /* check that we are not reinterpreting memory containing Objects. */ if (_may_have_objects(PyArray_DESCR(self)) || _may_have_objects(typed)) { - npy_cache_import("numpy._core._internal", "_getfield_is_safe", - &npy_thread_unsafe_state._getfield_is_safe); - if (npy_thread_unsafe_state._getfield_is_safe == NULL) { + if (npy_cache_import_runtime( + "numpy._core._internal", "_getfield_is_safe", + &npy_runtime_imports._getfield_is_safe) == -1) { Py_DECREF(typed); return NULL; } /* only returns True or raises */ - safe = PyObject_CallFunction(npy_thread_unsafe_state._getfield_is_safe, + safe = PyObject_CallFunction(npy_runtime_imports._getfield_is_safe, "OOi", PyArray_DESCR(self), typed, offset); if (safe == NULL) { @@ -2248,18 +2246,19 @@ NPY_NO_EXPORT int PyArray_Dump(PyObject *self, PyObject *file, int protocol) { PyObject *ret; - npy_cache_import("numpy._core._methods", "_dump", - &npy_thread_unsafe_state._dump); - if (npy_thread_unsafe_state._dump == NULL) { + if (npy_cache_import_runtime( + "numpy._core._methods", "_dump", + &npy_runtime_imports._dump) == -1) { return -1; } + if (protocol < 0) { ret = PyObject_CallFunction( - npy_thread_unsafe_state._dump, "OO", self, file); + npy_runtime_imports._dump, "OO", self, file); } else { ret = PyObject_CallFunction( - npy_thread_unsafe_state._dump, "OOi", self, file, protocol); + npy_runtime_imports._dump, "OOi", self, file, protocol); } if (ret == NULL) { return -1; @@ -2272,17 +2271,16 @@ PyArray_Dump(PyObject *self, PyObject *file, int protocol) NPY_NO_EXPORT PyObject * PyArray_Dumps(PyObject *self, int protocol) { - npy_cache_import("numpy._core._methods", "_dumps", - &npy_thread_unsafe_state._dumps); - if (npy_thread_unsafe_state._dumps == NULL) { + if (npy_cache_import_runtime("numpy._core._methods", "_dumps", + &npy_runtime_imports._dumps) == -1) { return NULL; } if (protocol < 0) { - return PyObject_CallFunction(npy_thread_unsafe_state._dumps, "O", self); + return PyObject_CallFunction(npy_runtime_imports._dumps, "O", self); } else { return PyObject_CallFunction( - npy_thread_unsafe_state._dumps, "Oi", self, protocol); + npy_runtime_imports._dumps, "Oi", self, protocol); } } diff --git a/numpy/_core/src/multiarray/multiarraymodule.c b/numpy/_core/src/multiarray/multiarraymodule.c index e8bc75c1e359..ac0bdc8dafe2 100644 --- a/numpy/_core/src/multiarray/multiarraymodule.c +++ b/numpy/_core/src/multiarray/multiarraymodule.c @@ -4842,6 +4842,10 @@ PyMODINIT_FUNC PyInit__multiarray_umath(void) { goto err; } + if (init_import_mutex() < 0) { + goto err; + } + if (init_extobj() < 0) { goto err; } @@ -5067,14 +5071,15 @@ PyMODINIT_FUNC PyInit__multiarray_umath(void) { * init_string_dtype() but that needs to happen after * the legacy dtypemeta classes are available. */ - npy_cache_import("numpy.dtypes", "_add_dtype_helper", - &npy_thread_unsafe_state._add_dtype_helper); - if (npy_thread_unsafe_state._add_dtype_helper == NULL) { + + if (npy_cache_import_runtime( + "numpy.dtypes", "_add_dtype_helper", + &npy_runtime_imports._add_dtype_helper) == -1) { goto err; } if (PyObject_CallFunction( - npy_thread_unsafe_state._add_dtype_helper, + npy_runtime_imports._add_dtype_helper, "Os", (PyObject *)&PyArray_StringDType, NULL) == NULL) { goto err; } diff --git a/numpy/_core/src/multiarray/npy_static_data.c b/numpy/_core/src/multiarray/npy_static_data.c index 7f5e58dde21a..e8f554d40d9b 100644 --- a/numpy/_core/src/multiarray/npy_static_data.c +++ b/numpy/_core/src/multiarray/npy_static_data.c @@ -66,7 +66,7 @@ intern_strings(void) #define IMPORT_GLOBAL(base_path, name, object) \ assert(object == NULL); \ - npy_cache_import(base_path, name, &object); \ + object = npy_import(base_path, name); \ if (object == NULL) { \ return -1; \ } diff --git a/numpy/_core/src/multiarray/scalartypes.c.src b/numpy/_core/src/multiarray/scalartypes.c.src index a0517c247215..448e157ed2eb 100644 --- a/numpy/_core/src/multiarray/scalartypes.c.src +++ b/numpy/_core/src/multiarray/scalartypes.c.src @@ -608,15 +608,14 @@ _void_to_hex(const char* argbuf, const Py_ssize_t arglen, static PyObject * _void_scalar_to_string(PyObject *obj, int repr) { - npy_cache_import("numpy._core.arrayprint", - "_void_scalar_to_string", - &npy_thread_unsafe_state._void_scalar_to_string); - if (npy_thread_unsafe_state._void_scalar_to_string == NULL) { + if (npy_cache_import_runtime( + "numpy._core.arrayprint", "_void_scalar_to_string", + &npy_runtime_imports._void_scalar_to_string) == -1) { return NULL; } PyObject *is_repr = repr ? Py_True : Py_False; return PyObject_CallFunctionObjArgs( - npy_thread_unsafe_state._void_scalar_to_string, obj, is_repr, NULL); + npy_runtime_imports._void_scalar_to_string, obj, is_repr, NULL); } static PyObject * diff --git a/numpy/_core/src/multiarray/strfuncs.c b/numpy/_core/src/multiarray/strfuncs.c index 759c730c7cfa..efe5c8a4fdd8 100644 --- a/numpy/_core/src/multiarray/strfuncs.c +++ b/numpy/_core/src/multiarray/strfuncs.c @@ -38,15 +38,14 @@ array_repr(PyArrayObject *self) * We need to do a delayed import here as initialization on module load * leads to circular import problems. */ - npy_cache_import("numpy._core.arrayprint", "_default_array_repr", - &npy_thread_unsafe_state._default_array_repr); - if (npy_thread_unsafe_state._default_array_repr == NULL) { + if (npy_cache_import_runtime("numpy._core.arrayprint", "_default_array_repr", + &npy_runtime_imports._default_array_repr) == -1) { npy_PyErr_SetStringChained(PyExc_RuntimeError, "Unable to configure default ndarray.__repr__"); return NULL; } return PyObject_CallFunctionObjArgs( - npy_thread_unsafe_state._default_array_repr, self, NULL); + npy_runtime_imports._default_array_repr, self, NULL); } @@ -57,15 +56,15 @@ array_str(PyArrayObject *self) * We need to do a delayed import here as initialization on module load leads * to circular import problems. */ - npy_cache_import("numpy._core.arrayprint", "_default_array_str", - &npy_thread_unsafe_state._default_array_str); - if (npy_thread_unsafe_state._default_array_str == NULL) { + if (npy_cache_import_runtime( + "numpy._core.arrayprint", "_default_array_str", + &npy_runtime_imports._default_array_str) == -1) { npy_PyErr_SetStringChained(PyExc_RuntimeError, "Unable to configure default ndarray.__str__"); return NULL; } return PyObject_CallFunctionObjArgs( - npy_thread_unsafe_state._default_array_str, self, NULL); + npy_runtime_imports._default_array_str, self, NULL); } diff --git a/numpy/_core/src/multiarray/stringdtype/dtype.c b/numpy/_core/src/multiarray/stringdtype/dtype.c index 038fa8159171..81a846bf6d96 100644 --- a/numpy/_core/src/multiarray/stringdtype/dtype.c +++ b/numpy/_core/src/multiarray/stringdtype/dtype.c @@ -715,21 +715,20 @@ stringdtype_repr(PyArray_StringDTypeObject *self) static PyObject * stringdtype__reduce__(PyArray_StringDTypeObject *self, PyObject *NPY_UNUSED(args)) { - npy_cache_import("numpy._core._internal", "_convert_to_stringdtype_kwargs", - &npy_thread_unsafe_state._convert_to_stringdtype_kwargs); - - if (npy_thread_unsafe_state._convert_to_stringdtype_kwargs == NULL) { + if (npy_cache_import_runtime( + "numpy._core._internal", "_convert_to_stringdtype_kwargs", + &npy_runtime_imports._convert_to_stringdtype_kwargs) == -1) { return NULL; } if (self->na_object != NULL) { return Py_BuildValue( - "O(iO)", npy_thread_unsafe_state._convert_to_stringdtype_kwargs, + "O(iO)", npy_runtime_imports._convert_to_stringdtype_kwargs, self->coerce, self->na_object); } return Py_BuildValue( - "O(i)", npy_thread_unsafe_state._convert_to_stringdtype_kwargs, + "O(i)", npy_runtime_imports._convert_to_stringdtype_kwargs, self->coerce); } diff --git a/numpy/_core/src/umath/funcs.inc.src b/numpy/_core/src/umath/funcs.inc.src index 3825bd869468..1075af97c9df 100644 --- a/numpy/_core/src/umath/funcs.inc.src +++ b/numpy/_core/src/umath/funcs.inc.src @@ -192,12 +192,11 @@ npy_ObjectGCD(PyObject *i1, PyObject *i2) /* otherwise, use our internal one, written in python */ { - npy_cache_import("numpy._core._internal", "_gcd", - &npy_thread_unsafe_state.internal_gcd_func); - if (npy_thread_unsafe_state.internal_gcd_func == NULL) { + if (npy_cache_import_runtime("numpy._core._internal", "_gcd", + &npy_runtime_imports.internal_gcd_func) == -1) { return NULL; } - gcd = PyObject_CallFunction(npy_thread_unsafe_state.internal_gcd_func, + gcd = PyObject_CallFunction(npy_runtime_imports.internal_gcd_func, "OO", i1, i2); if (gcd == NULL) { return NULL; diff --git a/numpy/_core/src/umath/override.c b/numpy/_core/src/umath/override.c index 55cca0857229..139d9c7bdbbd 100644 --- a/numpy/_core/src/umath/override.c +++ b/numpy/_core/src/umath/override.c @@ -369,13 +369,14 @@ PyUFunc_CheckOverride(PyUFuncObject *ufunc, char *method, /* All tuple items must be set before use */ Py_INCREF(Py_None); PyTuple_SET_ITEM(override_args, 0, Py_None); - npy_cache_import( + if (npy_cache_import_runtime( "numpy._core._internal", "array_ufunc_errmsg_formatter", - &npy_thread_unsafe_state.array_ufunc_errmsg_formatter); - assert(npy_thread_unsafe_state.array_ufunc_errmsg_formatter != NULL); + &npy_runtime_imports.array_ufunc_errmsg_formatter) == -1) { + goto fail; + } errmsg = PyObject_Call( - npy_thread_unsafe_state.array_ufunc_errmsg_formatter, + npy_runtime_imports.array_ufunc_errmsg_formatter, override_args, normal_kwds); if (errmsg != NULL) { PyErr_SetObject(PyExc_TypeError, errmsg); diff --git a/numpy/_core/src/umath/ufunc_object.c b/numpy/_core/src/umath/ufunc_object.c index a0acaf6573ed..3715866a2a83 100644 --- a/numpy/_core/src/umath/ufunc_object.c +++ b/numpy/_core/src/umath/ufunc_object.c @@ -5228,7 +5228,8 @@ prepare_input_arguments_for_outer(PyObject *args, PyUFuncObject *ufunc) { PyArrayObject *ap1 = NULL; PyObject *tmp; - npy_cache_import("numpy", "matrix", &npy_thread_unsafe_state.numpy_matrix); + npy_cache_import_runtime("numpy", "matrix", + &npy_runtime_imports.numpy_matrix); const char *matrix_deprecation_msg = ( "%s.outer() was passed a numpy matrix as %s argument. " @@ -5239,7 +5240,7 @@ prepare_input_arguments_for_outer(PyObject *args, PyUFuncObject *ufunc) tmp = PyTuple_GET_ITEM(args, 0); - if (PyObject_IsInstance(tmp, npy_thread_unsafe_state.numpy_matrix)) { + if (PyObject_IsInstance(tmp, npy_runtime_imports.numpy_matrix)) { /* DEPRECATED 2020-05-13, NumPy 1.20 */ if (PyErr_WarnFormat(PyExc_DeprecationWarning, 1, matrix_deprecation_msg, ufunc->name, "first") < 0) { @@ -5256,7 +5257,7 @@ prepare_input_arguments_for_outer(PyObject *args, PyUFuncObject *ufunc) PyArrayObject *ap2 = NULL; tmp = PyTuple_GET_ITEM(args, 1); - if (PyObject_IsInstance(tmp, npy_thread_unsafe_state.numpy_matrix)) { + if (PyObject_IsInstance(tmp, npy_runtime_imports.numpy_matrix)) { /* DEPRECATED 2020-05-13, NumPy 1.20 */ if (PyErr_WarnFormat(PyExc_DeprecationWarning, 1, matrix_deprecation_msg, ufunc->name, "second") < 0) { @@ -6401,12 +6402,9 @@ ufunc_get_doc(PyUFuncObject *ufunc, void *NPY_UNUSED(ignored)) { PyObject *doc; - npy_cache_import( - "numpy._core._internal", - "_ufunc_doc_signature_formatter", - &npy_thread_unsafe_state._ufunc_doc_signature_formatter); - - if (npy_thread_unsafe_state._ufunc_doc_signature_formatter == NULL) { + if (npy_cache_import_runtime( + "numpy._core._internal", "_ufunc_doc_signature_formatter", + &npy_runtime_imports._ufunc_doc_signature_formatter) == -1) { return NULL; } @@ -6415,8 +6413,9 @@ ufunc_get_doc(PyUFuncObject *ufunc, void *NPY_UNUSED(ignored)) * introspection on name and nin + nout to automate the first part * of it the doc string shouldn't need the calling convention */ - doc = PyObject_CallFunctionObjArgs(npy_thread_unsafe_state._ufunc_doc_signature_formatter, - (PyObject *)ufunc, NULL); + doc = PyObject_CallFunctionObjArgs( + npy_runtime_imports._ufunc_doc_signature_formatter, + (PyObject *)ufunc, NULL); if (doc == NULL) { return NULL; } diff --git a/numpy/_core/src/umath/ufunc_type_resolution.c b/numpy/_core/src/umath/ufunc_type_resolution.c index b523bd0b4d83..cabcff3b9bef 100644 --- a/numpy/_core/src/umath/ufunc_type_resolution.c +++ b/numpy/_core/src/umath/ufunc_type_resolution.c @@ -35,10 +35,9 @@ #include "npy_config.h" #include "numpy/npy_common.h" -#include "npy_import.h" - #include "numpy/ndarraytypes.h" #include "numpy/ufuncobject.h" +#include "npy_import.h" #include "ufunc_type_resolution.h" #include "ufunc_object.h" #include "common.h" diff --git a/numpy/_core/src/umath/umathmodule.c b/numpy/_core/src/umath/umathmodule.c index 5402b17c399a..0c8fc4857ea7 100644 --- a/numpy/_core/src/umath/umathmodule.c +++ b/numpy/_core/src/umath/umathmodule.c @@ -22,6 +22,7 @@ #include "numpy/ufuncobject.h" #include "numpy/npy_3kcompat.h" #include "npy_pycompat.h" +#include "npy_argparse.h" #include "abstract.h" #include "numpy/npy_math.h" @@ -321,5 +322,9 @@ int initumath(PyObject *m) return -1; } + if (init_argparse_mutex() < 0) { + return -1; + } + return 0; } diff --git a/numpy/_core/tests/test_argparse.py b/numpy/_core/tests/test_argparse.py index cddee72ea04c..ededced3b9fe 100644 --- a/numpy/_core/tests/test_argparse.py +++ b/numpy/_core/tests/test_argparse.py @@ -11,10 +11,29 @@ def func(arg1, /, arg2, *, arg3): return None """ +import threading + import pytest import numpy as np -from numpy._core._multiarray_tests import argparse_example_function as func +from numpy._core._multiarray_tests import ( + argparse_example_function as func, + threaded_argparse_example_function as thread_func, +) +from numpy.testing import IS_WASM + + +@pytest.mark.skipif(IS_WASM, reason="wasm doesn't have support for threads") +def test_thread_safe_argparse_cache(): + b = threading.Barrier(8) + + def call_thread_func(): + b.wait() + thread_func(arg1=3, arg2=None) + + tasks = [threading.Thread(target=call_thread_func) for _ in range(8)] + [t.start() for t in tasks] + [t.join() for t in tasks] def test_invalid_integers(): diff --git a/numpy/meson.build b/numpy/meson.build index 7e9ec5244cc9..032cdd5c6b60 100644 --- a/numpy/meson.build +++ b/numpy/meson.build @@ -214,6 +214,51 @@ else lapack_dep = declare_dependency(dependencies: [lapack, blas_dep]) endif +# Determine whether it is necessary to link libatomic with gcc. This +# could be the case on 32-bit platforms when atomic operations are used +# on 64-bit types or on RISC-V using 8-bit atomics, so we explicitly +# check for both 64 bit and 8 bit operations. The check is adapted from +# SciPy, who copied it from Mesa. +null_dep = dependency('', required : false) +atomic_dep = null_dep +code_non_lockfree = ''' + #include + int main() { + struct { + void *p; + uint8_t u8v; + } x; + x.p = NULL; + x.u8v = 0; + uint8_t res = __atomic_load_n(x.u8v, __ATOMIC_SEQ_CST); + __atomic_store_n(x.u8v, 1, ATOMIC_SEQ_CST); + void *p = __atomic_load_n(x.p, __ATOMIC_SEQ_CST); + __atomic_store_n((void **)x.p, NULL, __ATOMIC_SEQ_CST) + return 0; + } +''' +if cc.get_id() != 'msvc' + if not cc.links( + code_non_lockfree, + name : 'Check atomic builtins without -latomic' + ) + atomic_dep = cc.find_library('atomic', required: false) + if atomic_dep.found() + # We're not sure that with `-latomic` things will work for all compilers, + # so verify and only keep libatomic as a dependency if this works. It is + # possible the build will fail later otherwise - unclear under what + # circumstances (compilers, runtimes, etc.) exactly and this may need to + # be extended when support is added for new CPUs + if not cc.links( + code_non_lockfree, + dependencies: atomic_dep, + name : 'Check atomic builtins with -latomic' + ) + atomic_dep = null_dep + endif + endif + endif +endif # Copy the main __init__.py|pxd files to the build dir (needed for Cython) __init__py = fs.copyfile('__init__.py')