Thread safety¶
Chalc supports concurrent computations on independent data, and concurrent reads of shared data that stays unchanged. Chalc does not lock its computational objects for you. If one thread may change shared data while another reads or changes it, both threads must use the same lock. Unsynchronized reads and writes to native filtration data can produce incorrect results or crash Python.
These rules apply with or without Python’s global interpreter lock (GIL). A free-threaded Python build lets Python threads run at the same time; it does not make changes to shared objects safe automatically.
Independent computations¶
Each worker can create and change its own inputs and filtration without a lock:
from concurrent.futures import ThreadPoolExecutor
import chalc as ch
import numpy as np
def analyse(seed):
rng = np.random.default_rng(seed)
points = rng.random((2, 100))
colours = rng.integers(0, 2, size=100, dtype=np.uint16)
filtration = ch.chromatic.alpha(points, colours, max_num_threads=1)
return filtration.boundary_matrix()
with ThreadPoolExecutor(max_workers=4) as pool:
matrices = list(pool.map(analyse, range(8)))
Here each worker has its own arrays and filtration. max_num_threads=1 keeps
each geometry call serial internally while the workers run separate calls.
Chalc can also use multiple threads within each call. When using a thread pool,
consider limiting the threads within each call to avoid creating more work than
your machine can run at once.
Geometry functions can share input arrays if those arrays stay unchanged. Do not write to or resize points or colours during a call. This includes changes through another view of the same NumPy array. Chalc copies the inputs into C++ storage, but another thread must not change them while that copy is being made.
Filtration morphisms¶
SubChromaticInclusion, KChromaticInclusion, SubChromaticGluing, and
KChromaticGluing retain the filtration supplied to their constructor.
Creating two morphisms from the same filtration does not make two independent
filtrations.
Concurrent sixpack() calls may safely share a morphism object if its parameters
and underlying filtration stay unchanged. If either may change, use the same lock
for changing them and for the whole sixpack() call. Custom subclasses must also
ensure that their own methods and state allow concurrent calls.
Persistence modules and six-packs¶
AugmentedPersistenceModule exposes its pairings and entrance times for
reading. Its queries may run concurrently. Do not change its private attributes
or make its entrance-time array writable while other threads use it.
Several threads may read a SixPack if its data stays unchanged. Its
SixPackPairings, SixPackRepresentatives, and SixPackMorphisms containers
are frozen dataclasses, but their dictionaries and NumPy arrays can still be
changed. metadata can also be changed, and may contain user objects with
their own threading restrictions. Protect conflicting reads and writes with
the same lock.
SixPack takes ownership of the dataclasses passed to its constructor and
reindexes their contents during construction. Do not use those dataclasses in
another thread while construction is in progress. Passing the same dataclasses
to separate constructors does not create independent data. Make copies first
if you need independent inputs.
Plotting and HDF5 files¶
Use chalc’s plotting and animation functions on the main thread. They use Matplotlib, whose GUI backends generally require the main thread and whose objects are not generally safe to access concurrently. See Matplotlib’s threading guidance.
to_hdf5() reads the object being saved, so that object must remain unchanged
throughout the call. Separate threads can save to separate files. If threads
share a file or group, use a common lock around each complete save or load,
including opening and closing the file when applicable. A save or load involves
several dataset operations; h5py’s internal lock does not make the whole sequence
indivisible. Also prevent changes to a file while another thread is loading it.
See h5py’s threading documentation.
Free-threaded Python¶
Install chalc and its dependencies for the free-threaded interpreter you use. An extension that does not declare support may turn the GIL back on when it is imported. Check after importing chalc and any other libraries your program uses:
import sys
import sysconfig
import chalc
print(sysconfig.get_config_var("Py_GIL_DISABLED")) # 1 for a free-threaded build
print(sys._is_gil_enabled()) # False when the GIL is disabled
The tests in tests/test_thread_safety.py exercise independent computations,
shared reads, and updates protected by caller locks. They run on regular Python
too. On a free-threaded build they also check that importing chalc leaves the GIL
disabled. These tests cover the supported usage discussed above; they do not make
unsynchronized changes to shared objects safe.