Locking

Added in version 1.5: The fits rwlock, pref_rwlock, stars_lock, the worker queue lock and the cairo_mutex are all 1.5 additions.

This is the single authoritative reference for every synchronisation primitive in the codebase: what it guards, who may take it, and — most importantly — where it must not be taken. This knowledge is otherwise scattered across comment blocks in siril.h, processing_thread.c, processing.c and the GUI sources. Deadlocks and lock-flavour mismatches here have caused shipped bugs, so treat this page as normative.

For who runs on which thread see Threading; for what the workers lock on your behalf see Generic workers.

Note

The inventory below was regenerated from master with grep -rnE "(GMutex|GRWLock|GCond|GRecMutex|omp_lock_t)\s+[a-z_]+" src. If you add a lock, add a row here.

Inventory

Lock

Type

Declared in

Guards

com.mutex

GMutex

core/siril.h

Log-message ring buffer; gui.hi/gui.lo remap snapshots; com.selection (four fields, atomically); com.kernel/kernelsize/kernelchannels.

com.env_mutex

GMutex

core/siril.h

g_setenv() / g_unsetenv() (glib env calls are not thread-safe).

com.histogram_mutex

GMutex

core/siril.h

com.layers_hist[] and com.sat_hist.

com.pref_rwlock

GRWLock

core/siril.h

com.pref: workers hold a reader lock for the whole job; pref writers (process_set_*, Python, prefs dialog) take brief writer locks.

com.stars_lock

GRWLock

core/siril.h

com.stars / star_is_seqdata: writer to add/replace/free, reader to iterate.

fits rwlock

GRWLock

core/siril.h (last member of struct ffit)

Pixel data + metadata of public fits (currently gfit) against workers and Python threads.

queue_mutex / queue_cond

GMutex / GCond

core/processing_thread.c

The job queue, job_active_flag, and the Python reservation.

per-job mutex / cond

GMutex / GCond

core/processing_thread.c

A blocking submitter waiting for its job's completion.

child_mutex

GMutex

core/processing.c

com.children (spawned-subprocess list).

generic_seq_args.lock

omp_lock_t

core/processing.h

Cross-thread accumulation inside sequence hooks (worker-initialised).

pool_mutex / pool_cond

GMutex / GCond

io/seqwriter.c

Write-queue backpressure for the sequence writer.

fd_lock / ts_lock

omp_lock_t

io/ser.h (per ser_struct)

A SER file's descriptor and its timestamp table.

pool_mutex / pool_cond

GMutex / GCond

io/conversion.h

The file-conversion worker pool.

write_mutex / read_mutex + conds

GMutex / GCond

core/pipe.c

The CLI named-pipe read and write queues.

gui.cairo_mutex

GMutex

gui-gtk4/gui_state.h

Tile/surface render buffers (held while buffers are read for rendering; materialise workers take it to install tiles).

roi_mutex

GMutex

gui-gtk4/callbacks.c

The ROI state; exposed cross-module as gui_iface.lock_roi_mutex / unlock_roi_mutex.

gui_mutex

GMutex

gui-gtk4/utils.c

The gui_function synchronous-dispatch state (plus a per-dispatch mutex/cond pair).

four ICC mutexes

GMutex

core/icc_profile.c

monitor_profile_mutex, soft_proof_profile_mutex, default_profiles_mutex, display_transform_mutex.

g_shm_mutex

GMutex

io/siril_pythonmodule.h

The SHM allocation-tracking list.

per-connection mutex / condition

GMutex / GCond

io/siril_pythonmodule.h

A Python IPC connection's request/response state.

init_mutex

GMutex

io/siril_pythonmodule.c

venv initialisation.

bgsamples_mutex

GMutex

algos/background_extraction.c

(module-local example) background sample list.

gui_repo_scripts_mutex

GMutex

io/siril_git.c

(module-local example) the fetched-scripts list.

Per-lock rules for the big five

fits rwlock

Guards gfit's pixels and metadata. Locked only in high-level code: the generic workers, simple functions run via start_in_new_thread, and Python command handlers. It is never taken in low-level fits functions (format loaders, copyfits, direct buffer access) — the comment in siril.h says so explicitly, because low-level locking deadlocks. Initialised by calloc/zero-init, never copied by copyfits(), and must remain the last struct member (a static assertion enforces this).

Important

Hook functions run with the fits already locked. A worker's image_hook / mask_hook must not take the rwlock on the fits it was handed — the lock is non-recursive, so on the in-place path you would self-deadlock instantly.

com.pref_rwlock

Workers hold a reader lock for the entire job, so every com.pref.* read inside a hook is already covered — do not lock it yourself. Writers (process_set_* command handlers, Python pref writes, the preferences dialog) take brief writer locks.

com.mutex

A general-purpose lock over several unrelated fields (see the table). Its most load-bearing duty is serialising the log ring buffer: the siril_log_* path takes com.mutex (see siril_log.c), which has a deadlock implication below.

com.histogram_mutex

Guards layers_hist[] / sat_hist. Follow the invalidate-then-update pairing: a pixel change invalidates the histogram, and the recompute takes this lock together with the gfit reader lock in an established order (below).

gui.cairo_mutex

Guards the render tile/surface buffers. Held while buffers are read to build a frame; the background materialise workers take it to install finished tiles. GUI-thread only concern — see Display and rendering.

Deadlock catalogue

Each entry is scenario → why → correct pattern.

Writer-locking ``pref_rwlock`` from inside a job. The worker already holds the reader lock for the whole job, and GRWLocks are neither recursive nor upgradable → self-deadlock. Correct: never write prefs from a hook; a set-pref command blocks until the current job finishes and writes then.

execute_idle_sync() from the GTK main thread. It waits for the main thread to run the idle — which is itself → deadlock. Correct: only call it from a worker thread; the gui_iface impl guards with an in-main-thread check (see gui_iface_impl.c).

Holding a lock across a synchronous hop to the main thread. Holding the fits rwlock (or anything the main thread might want) while calling execute_idle_sync deadlocks if the idle needs that lock. This is exactly why generic_image_worker's non-swap path unlocks before notify_gfit_data_modified (the notify copy_roi_into_gfit chain re-acquires gfit's writer lock). Correct rule: never hold a lock across a synchronous hop to another thread.

cancel_and_wait_for_preview() vs a synchronous idle. It pumps the main loop while waiting for the job; if the job uses execute_idle_and_wait_for_it the two mutually wait. Correct: preview-style jobs must use asynchronous idles only (stated in processing_thread.h).

Locking a fits in low-level code. Readers/writers of data/fdata, copyfits, and format loaders must not take the rwlock — locking belongs to the high-level caller (siril.h).

Reader/writer flavour mismatch. Unlock with the same flavour you locked: g_rw_lock_reader_lock pairs with g_rw_lock_reader_unlock. A reader-lock/writer-unlock mismatch corrupts the lock state and shipped as a stacking bug — glib does not assert, so the corruption is silent.

GMutex is not recursive + logging. Any function that logs via siril_log_* takes com.mutex on that path (siril_log.c). Do not call it while already holding com.mutex, and more generally: know which locks your callees take before locking.

claim_thread_for_python() circular wait. It waits on queue_cond while a job is active. Never call it from code the active job can itself be waiting on.

Histogram lock ordering. The histogram recompute takes the gfit reader lock and histogram_mutex together; take them in the established order (gfit reader lock first — see the recompute path in single_image.c) and require new code to follow it.

Guidance for new code

  • Prefer a module-static GMutex over adding anything to com. A static GMutex / GRWLock needs no explicit initialisation. bgsamples_mutex and gui_repo_scripts_mutex are the pattern to copy.

  • If a lock becomes cross-module, declare it in the owning header, document it in the siril.h comment-block style, and add a row to the table above.

  • Do not expose a lock through gui_iface unless both sides genuinely need it — roi_mutex is the sole precedent.

  • Prefer message passing (idle functions, the worker queue) over shared state wherever you can.

See also