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}