|
| 1 | +/// \file |
| 2 | +/// \ingroup tutorial_dataframe |
| 3 | +/// \notebook -nodraw |
| 4 | +/// Usage of multithreading mode with random generators. |
| 5 | +/// |
| 6 | +/// This example illustrates how to define functions that generate random numbers and use them in an RDataFrame |
| 7 | +/// computation graph in a thread-safe way. |
| 8 | +/// |
| 9 | +/// Using only one random number generator in an application running with ROOT::EnableImplicitMT() is a common pitfall. |
| 10 | +/// This pitfall creates race conditions resulting in a distorted random distribution. In the example, this issue is |
| 11 | +/// solved by creating one random number generator per RDataFrame processing slot, thus allowing for parallel and |
| 12 | +/// thread-safe access. The example also illustrates the difference between non-deterministic and deterministic random |
| 13 | +/// number generation. |
| 14 | +/// |
| 15 | +/// \macro_code |
| 16 | +/// \macro_image |
| 17 | +/// \macro_output |
| 18 | +/// |
| 19 | +/// \date February 2026 |
| 20 | +/// \author Bohdan Dudar (JGU Mainz), Fernando Hueso-González (IFIC, CSIC-UV), Vincenzo Eduardo Padulano (CERN) |
| 21 | + |
| 22 | +#include <iostream> |
| 23 | +#include <memory> |
| 24 | +#include <TCanvas.h> |
| 25 | +#include <ROOT/RDataFrame.hxx> |
| 26 | + |
| 27 | +#include "df041_ThreadSafeRNG.hxx" |
| 28 | + |
| 29 | +// Canvas that should survive the running of this macro |
| 30 | +std::unique_ptr<TCanvas> myCanvas; |
| 31 | + |
| 32 | +void df041_ThreadSafeRNG() |
| 33 | +{ |
| 34 | + myCanvas = std::make_unique<TCanvas>("myCanvas", "myCanvas", 1000, 500); |
| 35 | + myCanvas->Divide(3, 1); |
| 36 | + |
| 37 | + unsigned int nEntries{10000000}; |
| 38 | + |
| 39 | + // 1. Single thread for reference |
| 40 | + auto df1 = ROOT::RDataFrame(nEntries).Define("x", GetNormallyDistributedNumberFromGlobalGenerator); |
| 41 | + auto h1 = df1.Histo1D({"h1", "Single thread (no MT)", 1000, -4, 4}, {"x"}); |
| 42 | + myCanvas->cd(1); |
| 43 | + h1->DrawCopy(); |
| 44 | + |
| 45 | + // 2. One generator per RDataFrame slot, with random_device seeding |
| 46 | + // Notes and Caveats: |
| 47 | + // - How many numbers are drawn from each generator is not deterministic |
| 48 | + // and the result is not deterministic between runs. |
| 49 | + unsigned int nSlots{8}; |
| 50 | + ROOT::EnableImplicitMT(nSlots); |
| 51 | + // Before running the RDataFrame computation graph, we reinitialize the generators (one per slot), so they can |
| 52 | + // be used accordingly during the execution. |
| 53 | + ReinitializeGenerators(nSlots); |
| 54 | + auto df2 = ROOT::RDataFrame(nEntries).DefineSlot("x", GetNormallyDistributedNumberPerSlotGenerator); |
| 55 | + auto h2 = df2.Histo1D({"h2", "Thread-safe (MT, non-deterministic)", 1000, -4, 4}, {"x"}); |
| 56 | + myCanvas->cd(2); |
| 57 | + h2->DrawCopy(); |
| 58 | + |
| 59 | + // 3. One generator per RDataFrame slot, with entry seeding |
| 60 | + // Notes and Caveats: |
| 61 | + // - With RDataFrame(INTEGER_NUMBER) constructor (as in the example), |
| 62 | + // the result is deterministic and identical on every run |
| 63 | + // - With RDataFrame(TTree) constructor, the result is not guaranteed to be deterministic. |
| 64 | + // To make it deterministic, use something from the dataset to act as the event identifier |
| 65 | + // instead of rdfentry_, and use it as a seed. |
| 66 | + |
| 67 | + // Before running the RDataFrame computation graph, we reinitialize the generators (one per slot), so they can |
| 68 | + // be used accordingly during the execution. |
| 69 | + ReinitializeGenerators(nSlots); |
| 70 | + auto df3 = |
| 71 | + ROOT::RDataFrame(nEntries).DefineSlot("x", GetNormallyDistributedNumberPerSlotGeneratorForEntry, {"rdfentry_"}); |
| 72 | + auto h3 = df3.Histo1D({"h3", "Thread-safe (MT, deterministic)", 1000, -4, 4}, {"x"}); |
| 73 | + myCanvas->cd(3); |
| 74 | + h3->DrawCopy(); |
| 75 | + |
| 76 | + std::cout << std::fixed << std::setprecision(3) << "Final distributions : " << "Mean " << " +- " |
| 77 | + << "StdDev" << std::endl; |
| 78 | + std::cout << std::fixed << std::setprecision(3) << "Theoretical : " << "0.000" << " +- " |
| 79 | + << "1.000" << std::endl; |
| 80 | + std::cout << std::fixed << std::setprecision(3) << "Single thread (no MT) : " << h1->GetMean() << " +- " |
| 81 | + << h1->GetStdDev() << std::endl; |
| 82 | + std::cout << std::fixed << std::setprecision(3) << "Thread-safe (MT, non-deterministic): " << h2->GetMean() << " +- " |
| 83 | + << h2->GetStdDev() << std::endl; |
| 84 | + std::cout << std::fixed << std::setprecision(3) << "Thread-safe (MT, deterministic) : " << h3->GetMean() << " +- " |
| 85 | + << h3->GetStdDev() << std::endl; |
| 86 | +} |
0 commit comments