Skip to content

No protection: import numpy in two different threads can lead to race-condition #91238

Closed as duplicate

Description

@jhgoebbert
BPO 47082
Nosy @tiran, @seberg, @jhgoebbert

Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.

Show more details

GitHub fields:

assignee = None
closed_at = None
created_at = <Date 2022-03-21.17:03:05.346>
labels = ['interpreter-core', 'type-bug', '3.11']
title = 'No protection: `import numpy` in two different threads can lead to race-condition'
updated_at = <Date 2022-03-21.23:04:38.800>
user = 'https://github.com/jhgoebbert'

bugs.python.org fields:

activity = <Date 2022-03-21.23:04:38.800>
actor = 'seberg'
assignee = 'none'
closed = False
closed_date = None
closer = None
components = ['Interpreter Core']
creation = <Date 2022-03-21.17:03:05.346>
creator = 'jhgoebbert'
dependencies = []
files = []
hgrepos = []
issue_num = 47082
keywords = []
message_count = 4.0
messages = ['415687', '415690', '415718', '415722']
nosy_count = 3.0
nosy_names = ['christian.heimes', 'seberg', 'jhgoebbert']
pr_nums = []
priority = 'normal'
resolution = None
stage = None
status = 'open'
superseder = None
type = 'behavior'
url = 'https://bugs.python.org/issue47082'
versions = ['Python 3.11']

Activity

  1. jhgoebbert commented on Mar 21, 2022

    jhgoebbertmannequin
    MannequinAuthor

    While using Xpra we came across a bug which might be a Python or a NumPy issue.
    Perhaps some of you can help us understanding some internals.

    Calling import numpy at the same time in two different threads of the Python program can lead to a race-condition.
    This happens for example with Xpra when loading the encoder nvjpeg:

    2022-03-20 12:54:59,298  cannot load enc_nvjpeg (nvjpeg encoder)
    Traceback (most recent call last):
      File "<pythondir>/lib/python3.9/site-packages/xpra/codecs/loader.py", line 52, in codec_import_check
        ic =  __import__(class_module, {}, {}, classnames)
      File "xpra/codecs/nvjpeg/encoder.pyx", line 8, in init xpra.codecs.nvjpeg.encoder
      File "<pythondir>/lib/python3.9/site-packages/numpy/__init__.py", line 150, in <module>
        from . import core
      File "<pythondir>/lib/python3.9/site-packages/numpy/core/__init__.py", line 51, in <module>
        del os.environ[envkey]
      File "<pythondir>/lib/python3.9/os.py", line 695, in __delitem__
        raise KeyError(key) from None
    KeyError: 'OPENBLAS_MAIN_FREE'
    

    Here the environment variable OPENBLAS_MAIN_FREE is set in the numpy code:
    https://github.com/numpy/numpy/blob/maintenance/1.21.x/numpy/core/__init__.py#L18
    and short after that it is deleted
    https://github.com/numpy/numpy/blob/maintenance/1.21.x/numpy/core/__init__.py#L51
    But this deletion fails ... perhaps because the initialization runs at the same time in two threads 🤔

    Shouldn't Python protect us by design?

    @seberg comments HERE:

    So, my current hypothesis (and I have briefly checked the Python code) is that Python does not do manual locking. But it effectively locks due to this going into C and thus holding the GIL. But somewhere during the import of NumPy, NumPy probably releases the GIL briefly and that could allow the next thread to go into the import machinery.
    [..]
    NumPy may be doing some worse than typical stuff here, but right now it seems to me that Python should be protecting us.
    

    Can anyone comment on this?

  2. added
    3.11only security fixes
    interpreter-core(Objects, Python, Grammar, and Parser dirs)
    type-bugAn unexpected behavior, bug, or error
    on Mar 21, 2022
  3. seberg commented on Mar 21, 2022

    sebergmannequin
    Mannequin

    To add to this: it would seem to me that the side-effects of importing should be guaranteed to only be called once?

    However, IO or other operations could be part of the import side-effects and release the GIL. So even a simple, pure-Python, package could run into this same issue and probably won't even realize that they can run into it.
    (Assuming I understand how this is happening correctly.)

    So it would seem to me that either:

    • Python should lock on the thread level or maybe the sys.modules dictionary?
    • The threading module could somehow ensure safety by hooking into
      the import machinery?
    • Packages that may release the GIL or have side-effects that must
      only be run once have to lock (i.e. NumPy).
      (But it seems to me that many packages will not even be aware of
      possible issues.)
  4. tiran commented on Mar 21, 2022

    @tiran
    Member

    Python used to have a global import lock. The global import lock prevented recursive imports from other threads while the current thread was importing. The import lock was source of issues and dead locks. Antoine replaced it with a fine grained import lock in bpo-9260.

  5. seberg commented on Mar 21, 2022

    sebergmannequin
    Mannequin

    Thanks, so there should already be a lock in place (sorry, I missed that). But somehow we seem to get around it?

    Do you know what may cause the locking logic to fail in this case? Recursive imports in NumPy itself? Or Cython using low-level C-API?

    I.e. can you think of something to investigate that may help NumPy/Cython to make sure that locking is successful?

    /* Cythons Import code (slightly cleaned up for Python 3 only): */
    static PyObject *__Pyx_Import(PyObject *name, PyObject *from_list, int level) {
        PyObject *empty_list = 0;
        PyObject *module = 0;
        PyObject *global_dict = 0;
        PyObject *empty_dict = 0;
        PyObject *list;
        if (from_list)
            list = from_list;
        else {
            empty_list = PyList_New(0);
            if (!empty_list)
                goto bad;
            list = empty_list;
        }
        global_dict = PyModule_GetDict(__pyx_m);
        if (!global_dict)
            goto bad;
        empty_dict = PyDict_New();
        if (!empty_dict)
            goto bad;
        {
            if (level == -1) {
                if ((1) && (strchr(__Pyx_MODULE_NAME, '.'))) {
                    module = PyImport_ImportModuleLevelObject(
                        name, global_dict, empty_dict, list, 1);
                    if (!module) {
                        if (!PyErr_ExceptionMatches(PyExc_ImportError))
                            goto bad;
                        PyErr_Clear();
                    }
                }
                level = 0;
            }
            if (!module) {
                module = PyImport_ImportModuleLevelObject(
                    name, global_dict, empty_dict, list, level);
            }
        }
    bad:
        Py_XDECREF(empty_list);
        Py_XDECREF(empty_dict);
        return module;
    }
  6. transferred this issue fromon Apr 10, 2022
  7. fbrunodr commented on Apr 15, 2022

    @fbrunodr

    @seberg can you provide a working example of that bug? I tried to simulate this race condition and could not reproduce the bug.

  8. seberg commented on Apr 15, 2022

    @seberg
    Contributor

    Unfortunately not, it was @jhgoebbert who experienced it, but maybe I can try tomorrow or so. My hypothesis was cython got involved. But of course it could be that there is a deeper threading issue elsewhere. Another thing may be that you need a fairly new NumPy versions to run into the environment variable issue, because older versions probably do not mess with OPENBLAS_MAIN_FREE.

  9. cg505 commented on Apr 11, 2025

    @cg505

    Hi! We have an extremely fragile reproduction case in the CI for SkyPilot here: https://github.com/skypilot-org/skypilot/actions/runs/14391450370/job/40359190320?pr=5095
    Relevant stacktrace:

    E 04-10 22:26:00 sdk.py:1486] Traceback (most recent call last):
    E 04-10 22:26:00 sdk.py:1486]   File "/home/runner/work/skypilot/skypilot/sky/server/requests/executor.py", line 361, in _request_execution_wrapper
    E 04-10 22:26:00 sdk.py:1486]     return_value = func(**request_body.to_kwargs())
    E 04-10 22:26:00 sdk.py:1486]   File "/home/runner/work/skypilot/skypilot/sky/clouds/service_catalog/__init__.py", line 103, in list_accelerator_counts
    E 04-10 22:26:00 sdk.py:1486]     results = _map_clouds_catalog(clouds,
    E 04-10 22:26:00 sdk.py:1486]   File "/home/runner/work/skypilot/skypilot/sky/clouds/service_catalog/__init__.py", line 51, in _map_clouds_catalog
    E 04-10 22:26:00 sdk.py:1486]     results = subprocess_utils.run_in_parallel(_execute_catalog_method,
    E 04-10 22:26:00 sdk.py:1486]   File "/home/runner/work/skypilot/skypilot/sky/utils/subprocess_utils.py", line 138, in run_in_parallel
    E 04-10 22:26:00 sdk.py:1486]     return list(ordered_iterators)
    E 04-10 22:26:00 sdk.py:1486]   File "/home/runner/.local/share/uv/python/cpython-3.8.20-linux-x86_64-gnu/lib/python3.8/multiprocessing/pool.py", line 868, in next
    E 04-10 22:26:00 sdk.py:1486]     raise value
    E 04-10 22:26:00 sdk.py:1486]   File "/home/runner/.local/share/uv/python/cpython-3.8.20-linux-x86_64-gnu/lib/python3.8/multiprocessing/pool.py", line 125, in worker
    E 04-10 22:26:00 sdk.py:1486]     result = (True, func(*args, **kwds))
    E 04-10 22:26:00 sdk.py:1486]   File "/home/runner/work/skypilot/skypilot/sky/clouds/service_catalog/__init__.py", line 49, in _execute_catalog_method
    E 04-10 22:26:00 sdk.py:1486]     return method(*args, **kwargs)
    E 04-10 22:26:00 sdk.py:1486]   File "/home/runner/work/skypilot/skypilot/sky/clouds/service_catalog/vast_catalog.py", line 102, in list_accelerators
    E 04-10 22:26:00 sdk.py:1486]     return common.list_accelerators_impl('Vast', _df, gpus_only, name_filter,
    E 04-10 22:26:00 sdk.py:1486]   File "/home/runner/work/skypilot/skypilot/sky/clouds/service_catalog/common.py", line 578, in list_accelerators_impl
    E 04-10 22:26:00 sdk.py:1486]     df = df[~df['GpuInfo'].isna()]
    E 04-10 22:26:00 sdk.py:1486]   File "/home/runner/work/skypilot/skypilot/sky/clouds/service_catalog/common.py", line 153, in __getitem__
    E 04-10 22:26:00 sdk.py:1486]     return self._load_df()[key]
    E 04-10 22:26:00 sdk.py:1486]   File "/home/runner/work/skypilot/skypilot/sky/clouds/service_catalog/common.py", line 145, in _load_df
    E 04-10 22:26:00 sdk.py:1486]     raise e
    E 04-10 22:26:00 sdk.py:1486]   File "/home/runner/work/skypilot/skypilot/sky/clouds/service_catalog/common.py", line 138, in _load_df
    E 04-10 22:26:00 sdk.py:1486]     self._update_func()
    E 04-10 22:26:00 sdk.py:1486]   File "/home/runner/work/skypilot/skypilot/sky/clouds/service_catalog/common.py", line 209, in _update_catalog
    E 04-10 22:26:00 sdk.py:1486]     r = requests.get(url=url,
    E 04-10 22:26:00 sdk.py:1486]   File "/home/runner/test-env/lib/python3.8/site-packages/requests/api.py", line 73, in get
    E 04-10 22:26:00 sdk.py:1486]     return request("get", url, params=params, **kwargs)
    E 04-10 22:26:00 sdk.py:1486]   File "/home/runner/test-env/lib/python3.8/site-packages/requests/api.py", line 59, in request
    E 04-10 22:26:00 sdk.py:1486]     return session.request(method=method, url=url, **kwargs)
    E 04-10 22:26:00 sdk.py:1486]   File "/home/runner/test-env/lib/python3.8/site-packages/requests/sessions.py", line 579, in request
    E 04-10 22:26:00 sdk.py:1486]     settings = self.merge_environment_settings(
    E 04-10 22:26:00 sdk.py:1486]   File "/home/runner/test-env/lib/python3.8/site-packages/requests/sessions.py", line 760, in merge_environment_settings
    E 04-10 22:26:00 sdk.py:1486]     env_proxies = get_environ_proxies(url, no_proxy=no_proxy)
    E 04-10 22:26:00 sdk.py:1486]   File "/home/runner/test-env/lib/python3.8/site-packages/requests/utils.py", line 835, in get_environ_proxies
    E 04-10 22:26:00 sdk.py:1486]     return getproxies()
    E 04-10 22:26:00 sdk.py:1486]   File "/home/runner/.local/share/uv/python/cpython-3.8.20-linux-x86_64-gnu/lib/python3.8/urllib/request.py", line 2516, in getproxies_environment
    E 04-10 22:26:00 sdk.py:1486]     for name, value in os.environ.items():
    E 04-10 22:26:00 sdk.py:1486]   File "/home/runner/.local/share/uv/python/cpython-3.8.20-linux-x86_64-gnu/lib/python3.8/_collections_abc.py", line 744, in __iter__
    E 04-10 22:26:00 sdk.py:1486]     yield (key, self._mapping[key])
    E 04-10 22:26:00 sdk.py:1486]   File "/home/runner/.local/share/uv/python/cpython-3.8.20-linux-x86_64-gnu/lib/python3.8/os.py", line 675, in __getitem__
    E 04-10 22:26:00 sdk.py:1486]     raise KeyError(key) from None
    E 04-10 22:26:00 sdk.py:1486] KeyError: 'OPENBLAS_MAIN_FREE'
    E 04-10 22:26:00 sdk.py:1486] 
    

    We have some custom code to lazily import numpy as needed, and I guess it is running in one thread. It causes a crash in another thread - which is not doing an import, but is just trying to read from os.environ. In fact, you can see that the bottom three frames of the stacktrace are in CPython, and they all look very innocuous.

    It seems that the numpy import is somehow changing os.environ out from under the thread that crashes. That's easy enough to understand, but what I can't understand is how this is not protected by GIL, since numpy's os.environ modification is happening in Python:
    https://github.com/numpy/numpy/blob/v1.24.4/numpy/core/__init__.py#L14-L20
    https://github.com/numpy/numpy/blob/v1.24.4/numpy/core/__init__.py#L50-L52

    This is python 3.8.20 (I know EOL, but sharing anyway since it might help pin down the actual issue) with numpy 1.24.4. The threads are all part of a multiprocessing.pool.ThreadPool, which you can see in the stacktrace. It's here in the SkyPilot source:
    https://github.com/skypilot-org/skypilot/blob/d01d496e66615656a2ddf57e4695151392b61c63/sky/utils/subprocess_utils.py#L136-L138

    Unsurprisingly, it unfortunately seems very difficult to reproduce this in other environments.

  10. gpshead commented on Jan 11, 2026

    @gpshead
    Member

    I believe the root cause of this is #143650 - your numpy environment error matches the scenario that led me to debug this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    3.11only security fixesinterpreter-core(Objects, Python, Grammar, and Parser dirs)type-bugAn unexpected behavior, bug, or error

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions