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 |
|---|---|---|---|
|
GMutex |
|
Log-message ring buffer; |
|
GMutex |
|
|
|
GMutex |
|
|
|
GRWLock |
|
|
|
GRWLock |
|
|
fits |
GRWLock |
|
Pixel data + metadata of public fits (currently |
|
GMutex / GCond |
|
The job queue, |
per-job |
GMutex / GCond |
|
A blocking submitter waiting for its job's completion. |
|
GMutex |
|
|
|
omp_lock_t |
|
Cross-thread accumulation inside sequence hooks (worker-initialised). |
|
GMutex / GCond |
|
Write-queue backpressure for the sequence writer. |
|
omp_lock_t |
|
A SER file's descriptor and its timestamp table. |
|
GMutex / GCond |
|
The file-conversion worker pool. |
|
GMutex / GCond |
|
The CLI named-pipe read and write queues. |
|
GMutex |
|
Tile/surface render buffers (held while buffers are read for rendering; materialise workers take it to install tiles). |
|
GMutex |
|
The ROI state; exposed cross-module as
|
|
GMutex |
|
The |
four ICC mutexes |
GMutex |
|
|
|
GMutex |
|
The SHM allocation-tracking list. |
per-connection |
GMutex / GCond |
|
A Python IPC connection's request/response state. |
|
GMutex |
|
venv initialisation. |
|
GMutex |
|
(module-local example) background sample list. |
|
GMutex |
|
(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-
staticGMutex over adding anything tocom. A staticGMutex/GRWLockneeds no explicit initialisation.bgsamples_mutexandgui_repo_scripts_mutexare the pattern to copy.If a lock becomes cross-module, declare it in the owning header, document it in the
siril.hcomment-block style, and add a row to the table above.Do not expose a lock through
gui_ifaceunless both sides genuinely need it —roi_mutexis the sole precedent.Prefer message passing (idle functions, the worker queue) over shared state wherever you can.
See also
Threading — thread topology and job control.
Generic workers — what the workers lock for you.
Display and rendering —
cairo_mutexand the histogram pairing.Python integration — the fits lock from Python handlers.