These are the tools for integrating Numba kernels into threaded applications and C/SciPy APIs.
Compiled nopython code does not need the interpreter, but by default it still holds the GIL while running, so two Python threads calling the same compiled function run one after the other. @njit(nogil=True) releases the GIL on entry and reacquires it on return. Then a ThreadPoolExecutor can run several calls truly in parallel on different cores, with ordinary threads and shared memory, no prange needed.
You become responsible for thread safety: two threads writing the same array is a data race, just as in C. Releasing the GIL is also not possible in object mode or inside objmode blocks. On free-threaded Python builds the GIL may be absent anyway, but nogil=True remains the explicit way to declare compiled code safe to run concurrently.
@cfunc(signature) compiles a function with a C calling convention instead of a Python-callable dispatcher. Its .address is a raw function pointer and .ctypes a ctypes wrapper. Use it to pass callbacks to C libraries: for example scipy.integrate.quad and scipy.LowLevelCallable accept such pointers, removing the Python call overhead per evaluation, often a large speedup for integrators and optimisers. Inside a cfunc you cannot raise Python exceptions to the caller, and arguments must be C-compatible types (numbers and raw pointers, using carray to view pointers as arrays).
Related: Numba's ahead-of-time numba.pycc module is deprecated; for shipping compiled extensions, caching or Cython are the usual routes.
Example
import ctypes
import numpy as np
from concurrent.futures import ThreadPoolExecutor
from numba import njit, cfunc, float64
@njit(nogil=True)
def heavy(a):
s = 0.0
for x in a:
s += np.sin(x) ** 2
return s
chunks = np.array_split(np.random.default_rng(0).random(4_000_000), 4)
with ThreadPoolExecutor(4) as pool: # runs concurrently: GIL released
print(round(sum(pool.map(heavy, chunks)))) # 1090663
@cfunc(float64(float64))
def square(x):
return x * x
print(square.ctypes(3.0)) # 9.0 (called through a C pointer)
print(isinstance(square.address, int)) # TrueRemember
- nogil=True lets Python threads run compiled code concurrently
- You are responsible for avoiding data races once the GIL is released
- @cfunc produces a C function pointer for callbacks into C and SciPy
Watch out for
- Releasing the GIL and then writing to the same output array from several threads.