openmp_reducer/
lib.rs

1/*
2 * SPDX-FileCopyrightText: 2025 Tommaso Fontana
3 * SPDX-FileCopyrightText: 2025 Sebastiano Vigna
4 *
5 * SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later
6 */
7
8#![doc = include_str!("../README.md")]
9
10use std::fmt::Debug;
11use std::sync::Mutex;
12
13/// An OpenMP-style reducer that wraps a global value into a [`Mutex`],
14/// providing [shareable, cloneable copies with a local value](#method.share);
15/// the copies will be reduced into the global value when dropped.
16///
17/// The global value can be observed with [`peek`](Reducer::peek) if the base
18/// type is [`Clone`], whereas [`get`](Reducer::get) consumes self and returns
19/// the global value.
20///
21/// For convenience, the global value and the local value have distinct type
22/// parameters `G` and `L`, respectively; the second type defaults to the first
23/// one.
24///
25/// Each shared copy has a reference to the [`Reducer`] it was created from, so
26/// you cannot call [`get`](Reducer::get) if there are still shared copies
27/// around. For example, this code will not compile:
28/// ```compile_fail
29/// use openmp_reducer::Reducer;
30///
31/// let reducer = Reducer::<usize>::new(3, |global, local| *global += *local);
32/// let mut shared = reducer.share();
33/// // drop(shared); // uncommenting this line would make the code compile
34/// assert_eq!(reducer.get(), 3);
35///```
36///
37/// # Examples
38///
39/// In this example, we manually spawn processes:
40///
41/// ```rust
42/// use openmp_reducer::Reducer;
43/// use std::thread;
44///
45/// let mut reducer = Reducer::<usize>::new(5, |global, local| *global += *local);
46/// std::thread::scope(|s| {
47///     for i in 0..3 {
48///         let mut shared = reducer.share();
49///         s.spawn(move || {
50///             *shared.as_mut() += 10;
51///         });
52///     }
53/// });
54///
55/// // Initial value plus additional values from shared copies
56/// assert_eq!(reducer.get(), 35);
57/// ```
58///
59/// You can obtain the same behavior with [Rayon](https://docs.rs/rayon) using
60/// methods such as
61/// [`for_each_with`](https://docs.rs/rayon/latest/rayon/iter/trait.ParallelIterator.html#method.for_each_with)
62/// and
63/// [`map_with`](https://docs.rs/rayon/latest/rayon/iter/trait.ParallelIterator.html#method.map_with):
64///
65/// ```rust
66/// use openmp_reducer::Reducer;
67/// use rayon::prelude::*;
68///
69/// let mut reducer = Reducer::<usize>::new(5, |global, local| *global += *local);
70/// (0..1000000).into_par_iter().
71///     with_min_len(1000). // optional, might reduce the amount of cloning
72///     for_each_with(reducer.share(), |shared, i| {
73///         *shared.as_mut() += 1;
74///     }
75/// );
76///
77/// // Initial value plus additional values from clones
78/// assert_eq!(reducer.get(), 1_000_005);
79/// ```
80///
81/// Note that you have to pass `reducer.share()`, which can be cloned. Also,
82/// since
83/// [`for_each_with`](https://docs.rs/rayon/latest/rayon/iter/trait.ParallelIterator.html#method.for_each_with)
84/// might perform excessive cloning if jobs are too short, you can use
85/// [`with_min_len`](https://docs.rs/rayon/latest/rayon/iter/trait.ParallelIterator.html#method.with_min_len)
86/// to reduce the amount of cloning.
87#[derive(Debug)]
88pub struct Reducer<G: Debug + Default, L: Debug + Default = G> {
89    global: Mutex<G>,
90    reduce: fn(&mut G, &L),
91}
92
93impl<G: Debug + Default, L: Debug + Default> Reducer<G, L> {
94    /// Creates a new reducer with a given reduction function.
95    ///
96    /// The function must reduce the local value (second argument) into the
97    /// global value (first argument). For the result to be deterministic, the
98    /// global value must be the same regardless of the order in which the local
99    /// values are reduced.
100    pub fn new(init: G, reduce: fn(global: &mut G, local: &L)) -> Self {
101        Reducer {
102            global: Mutex::new(init),
103            reduce,
104        }
105    }
106
107    /// Returns a [`SharedReducer`] referencing this [`Reducer`].
108    ///
109    /// The [`SharedReducer`] will be initialized with the default value of the
110    /// base type.
111    pub fn share(&self) -> SharedReducer<G, L> {
112        SharedReducer {
113            openmp_reducer: self,
114            local: L::default(),
115        }
116    }
117
118    /// Consumes self and return the global value.
119    ///
120    /// Note that you cannot call this method if there are still [shared
121    /// copies](#method.share) that have not been dropped.
122    ///
123    /// If you just need to access the global value without consuming self, and
124    /// the base type is [`Clone`], use [`peek`](Reducer::peek).
125    ///
126    /// # Panics
127    ///
128    /// This method will panic if the mutex is poisoned.
129    /// [`peek`](Reducer::peek).
130    pub fn get(self) -> G {
131        self.global.into_inner().unwrap()
132    }
133}
134
135impl<G: Debug + Default + Clone, L: Debug + Default> Reducer<G, L> {
136    /// Returns the current global value.
137    ///
138    /// Note that this method does not guarantee that all shared copies have
139    /// been dropped. If you need that guarantee, use [`get`](Reducer::get).
140    ///
141    /// # Panics
142    ///
143    /// This method will panic if the mutex is poisoned.
144    pub fn peek(&self) -> G {
145        self.global.lock().unwrap().clone()
146    }
147}
148
149/// A shareable copy of a [`Reducer`] containing a local value and implementing
150/// [`Clone`].
151///
152/// The local value can be accessed with [`AsRef`] and [`AsMut`]
153/// implementations.
154///
155/// When a [`SharedReducer`] is dropped, the local value will be reduced into
156/// the global value.
157#[derive(Debug)]
158pub struct SharedReducer<'a, G: Debug + Default, L: Debug + Default> {
159    openmp_reducer: &'a Reducer<G, L>,
160    local: L,
161}
162
163impl<G: Debug + Default, L: Debug + Default> Clone for SharedReducer<'_, G, L> {
164    /// Returns a copy sharing the same global value and
165    /// with local value initialized to the default value.
166    fn clone(&self) -> Self {
167        SharedReducer {
168            openmp_reducer: self.openmp_reducer,
169            local: L::default(),
170        }
171    }
172}
173
174impl<G: Debug + Default, L: Debug + Default> Drop for SharedReducer<'_, G, L> {
175    /// Reduces the local value into the global value.
176    fn drop(&mut self) {
177        let mut lock = self.openmp_reducer.global.lock().unwrap();
178        (self.openmp_reducer.reduce)(&mut *lock, &self.local);
179    }
180}
181
182impl<G: Debug + Default + Clone, L: Debug + Default> SharedReducer<'_, G, L> {
183    /// Returns the current global value.
184    ///
185    /// This method delegates to [`Reducer::peek`].
186    pub fn peek(&self) -> G {
187        self.openmp_reducer.peek()
188    }
189}
190
191impl<G: Debug + Default, L: Debug + Default> AsRef<L> for SharedReducer<'_, G, L> {
192    /// Returns a reference to the local value.
193    fn as_ref(&self) -> &L {
194        &self.local
195    }
196}
197
198impl<G: Debug + Default, L: Debug + Default> AsMut<L> for SharedReducer<'_, G, L> {
199    /// Returns a mutable reference to the local value.
200    fn as_mut(&mut self) -> &mut L {
201        &mut self.local
202    }
203}