Locking ------- .. versionadded:: 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 :file:`siril.h`, :file:`processing_thread.c`, :file:`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 :ref:`threading:threading`; for *what the workers lock on your behalf* see :ref:`genericworkers: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 ========= .. list-table:: :header-rows: 1 :widths: 20 12 24 44 * - 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 :file:`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 :file:`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 :ref:`displayrendering: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 :file:`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 :file:`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 (:file:`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 (:file:`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 :file:`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 ======== - :ref:`threading:threading` — thread topology and job control. - :ref:`genericworkers:generic workers` — what the workers lock for you. - :ref:`displayrendering:display and rendering` — ``cairo_mutex`` and the histogram pairing. - :ref:`pythonintegration:python integration` — the fits lock from Python handlers.