22.3 Thread-Safe User Code
Table of Contents
Shared objects
When you enable multithreading in Geant4, your code runs in one master thread and several worker threads. Each worker has its own copies of most Geant4 kernel objects, but your user code can still introduce shared state by mistake. Thread safety is about making sure that accesses to any such shared state cannot interfere with each other.
A variable or object is effectively shared if it can be accessed from more than one worker thread at the same time. This typically happens in three situations: it has static storage duration (for example a global variable or static data member), it lives in a singleton, or you put it in some library that uses global state internally.
You can safely read the same immutable object from multiple threads. The problem appears when at least one thread writes to an object that other threads can see. Concurrent writes, or a write at the same time as another thread reads, can cause data races, crashes, or subtle errors.
To keep shared objects safe, first minimize them. Avoid global variables in your Geant4 application. If you must use a global object, design it to be read only after initialization. For example, a table of material names or detector IDs that never changes after setup is fine, as long as all threads see the same constant data.
For shared objects that must change at run time, you must coordinate access. The simplest mechanism in C++ is a mutex, for example std::mutex with std::lock_guard<std::mutex>. Wrap each mutation of the shared object within a lock, and be very careful not to call Geant4 kernel methods from inside the same critical region. Long or nested locks slow down multithreading and can even cause deadlocks if you are not careful with lock ordering.
Whenever possible, move shared mutable state out of the hot parts of the simulation. Avoid writing to shared containers from SteppingAction, TrackingAction, or sensitive detectors in worker threads. For instance, do not have every step push results into a single global std::vector or open a common output file. Instead, design the code so that each thread collects its own results locally, then combine them at well defined points in the master thread.
Some shared facilities, such as C standard I/O to stdout and stderr, as well as G4cout and G4cerr, are internally made safe by Geant4. However, heavy use of printing in many threads will still serialize output and reduce performance. Use verbose output selectively, mainly for debugging small test runs.
Random number engines are shared in concept but Geant4 handles them specially. Each worker gets its own engine instance and seed sequence, so you should not create and share your own global random engine for physics decisions. Always use the Geant4 random interface in user code that participates in the simulation flow.
External libraries can hide shared objects inside them. For example, some analysis libraries are not thread safe when multiple threads write to the same file or object. In a multithreaded Geant4 application, let each worker thread write to its own analysis buffers that are then merged, or use the Geant4 analysis manager in its recommended multithread setup instead of manually writing to a single output file from every thread.
Any mutable global or static variable that is written from more than one worker thread is not thread safe unless you explicitly protect every access with synchronization. Prefer immutable shared objects and replicate everything else per thread.
Per-thread data
A Geant4 multithreaded application is easiest to reason about if each worker thread has its own independent set of user objects. Geant4 enforces this pattern for many classes by construction. For example, RunAction, EventAction, and SteppingAction objects are created per worker through your ActionInitialization, and are not shared between threads.
You can extend this idea to your own data. Whenever you need to store state that logically belongs to a particular thread, keep it in a member of a user action class that Geant4 instantiates separately for each worker. For instance, you might give your RunAction a histogram accumulator, or your EventAction a set of event level counters. Each worker thread will have its own copy of those objects, so there is no need for locks while they are used in that thread.
Sometimes you need explicit per thread storage outside the usual Geant4 user actions. In that case, C++ thread local storage is useful. Declaring a variable as thread_local gives every thread its own instance, even if the variable is at global scope or a static class member. This is suitable for caches or auxiliary statistics that are accessed frequently from different places in the code but must not be shared between threads.
Per thread data needs to be combined at some point if you want global results. In a Geant4 simulation, this merging should happen in the master thread, typically at the end of a run. Geant4 provides built in mechanisms for this in its analysis manager, which collects per thread histograms and ntuples and merges them automatically. If you implement your own merging logic, design a well defined master side interface that takes worker results as inputs and never exposes shared mutable state back to the workers.
You should treat sensitive detectors and hit collections in the same way. Each worker thread creates and fills its own hit collections during events. At the end of a run, or during analysis, you either use the Geant4 tools that already know how to retrieve per event data, or you aggregate your per thread results in the master. Avoid passing pointers to thread local hits or collections from one thread to another.
Be aware that Geant4 does not support per event or per track objects shared across threads, since a single event or track always belongs to one worker. If you think you need to share an object between threads at event or track level, it is usually a sign that the design assumes sequential processing and needs to be refactored into independent per thread logic plus a later merge.
Design your user code so that:
- Simulation work in worker threads uses only per thread data or immutable shared data.
- Any merging of results happens in the master thread after workers have finished their part.
This pattern avoids data races and keeps your Geant4 application naturally thread safe.
Views: 9
KAHIBARO