18.8 Multithreading
Table of Contents
ROOT implicit multithreading
ROOT provides implicit multithreading to speed up many operations automatically on multicore machines. Instead of you managing threads directly, ROOT can split work across cores inside several high level interfaces, most importantly RDataFrame.
Implicit multithreading is controlled through ROOT::EnableImplicitMT() and related functions. You usually call this once at the beginning of your program or macro, before creating any RDataFrame or opening large TTrees. A typical C++ macro might start with
#include "ROOT/RDataFrame.hxx"
void analysis_mt() {
ROOT::EnableImplicitMT(); // use all available cores
ROOT::RDataFrame df("Events", "data.root");
// analysis goes here
}
When you call ROOT::EnableImplicitMT() without arguments, ROOT queries the system and chooses a reasonable number of threads, usually equal to the number of hardware cores. You can also request a specific number of threads, for example
ROOT::EnableImplicitMT(4); // force 4 worker threadsOnce implicit multithreading is enabled, RDataFrame, some I/O, and several math operations become thread parallel internally. You do not create or join threads yourself, and you do not change your event loop logic to a special multithreaded form. The same RDataFrame analysis code that worked without multithreading will usually work with multithreading after a single function call.
You can check whether implicit multithreading is currently active with
bool mt_on = ROOT::IsImplicitMTEnabled();
int nthreads = ROOT::GetImplicitMTPoolSize();You can disable it again with
ROOT::DisableImplicitMT();Although you can technically enable multithreading at any time, it is safest to do it at the very start of your program, before creating any RDataFrame instances or opening complex ROOT files. Switching the multithreading mode in the middle of an analysis that already constructed internal resources can lead to confusing behavior or reduced performance.
In PyROOT the interface is the same. An interactive Python session might look like
import ROOT
ROOT.ROOT.EnableImplicitMT() # note the extra ROOT namespace in Python
df = ROOT.RDataFrame("Events", "data.root")
# proceed as usualRDataFrame will then parallelize the event loop over several worker threads and will produce results that are logically identical to the single threaded run.
Always call ROOT::EnableImplicitMT() (or ROOT.ROOT.EnableImplicitMT() in Python) before constructing your RDataFrame and other heavy data structures, and write thread safe user code inside Define, Filter, and custom functions.
Parallel data processing
With implicit multithreading enabled, RDataFrame executes the event loop in parallel. Conceptually, RDataFrame splits the full set of entries of a TTree or data source into many smaller ranges, assigns different ranges to different worker threads, and processes them independently. Each thread runs the same chain of Filter, Define, and Histo* actions on its own subset of entries.
From the user point of view, you still write your analysis in a functional, step by step style. The parallelism appears when you trigger an action, such as Histo1D, Histo2D, Snapshot, Count, or statistical reducers. For example,
ROOT::EnableImplicitMT();
ROOT::RDataFrame df("Events", "data.root");
auto df_selected = df.Filter("energy > 1.0");
auto hE = df_selected.Histo1D("energy");
hE->Draw();
internally creates a pool of threads. Each thread reads a chunk of entries, applies the cut energy > 1.0, and fills a thread local histogram. At the end, RDataFrame merges all partial histograms into the final TH1 that you receive from Histo1D. The final result is the same as if you had processed all events sequentially.
This pattern applies to many actions. For Count, each thread counts how many entries pass the filters in its chunk, and RDataFrame adds these partial counts. For Snapshot, each thread writes its own portion of selected events, and ROOT coordinates safe writing to the output file.
For correct and efficient parallel data processing, your user code must satisfy a few important constraints.
First, any function used inside Define or Filter must be thread safe. It can safely read its inputs and return a value, but it must not write to shared global variables or modify shared objects unless you use explicit synchronization, which is advanced and usually avoidable. A typical thread safe Define is
auto df2 = df.Define("pt", "sqrt(px*px + py*py)");
This expression only uses local variables from each entry and does not touch global state, so it is safe. In contrast, code that updates a shared counter inside a Define or Filter would be unsafe.
Second, avoid creating or modifying ROOT graphics objects from inside a Define or Filter. Drawing and GUI elements are not designed for concurrent modification by multiple threads. The usual pattern is to let RDataFrame produce final histograms and graphs, and only after the RDataFrame actions are completed, draw these objects in the main thread.
Third, be aware of I/O. RDataFrame and ROOT handle reading from files in a thread aware way, including TChain and remote files, but if you open additional files or write custom logs inside Define or Filter, you must ensure that this I/O is also thread safe. Prefer writing logs or debug information after the event loop or protect custom output with your own mechanism if you really need it.
A parallel RDataFrame analysis often scales well with the number of cores. For CPU heavy operations on each event, speedups close to the number of threads are possible. For I/O heavy analyses, such as reading extremely large TTrees from slow storage, the gain may be smaller, because the disk or network becomes the main bottleneck.
If you want to control the number of threads to balance performance and resource use, call
ROOT::EnableImplicitMT(8); // for example, 8 threadson a machine with many cores when you want to leave capacity for other tasks, or to explore performance scaling.
In Python with PyROOT, the same pattern applies. An example:
import ROOT
ROOT.ROOT.EnableImplicitMT(4)
df = ROOT.RDataFrame("Events", "data.root")
df = df.Define("pt", "sqrt(px*px + py*py)").Filter("pt > 1.0")
h_pt = df.Histo1D(("h_pt", "p_{T};p_{T} [GeV];Events", 50, 0, 10), "pt")
h_pt.Draw()
Here the heavy work, reading events and computing pt, is split across four threads. Once Histo1D completes and returns, you use the resulting histogram as usual and draw it in the main thread.
Finally, you can combine implicit multithreading with other RDataFrame features such as friend trees, distributed data sources, or more complex filtering chains. The basic guideline is always the same: write your column definitions and filters as pure, local computations that depend only on the data of one entry, enable implicit multithreading at the start, and let ROOT manage the parallel event processing for you.
Inside Filter and Define, avoid writing to shared globals, avoid creating or modifying graphics objects, and avoid non thread safe I/O. Keep user code pure and entry local so that RDataFrame can safely process entries in parallel on multiple threads.
Views: 12
KAHIBARO