Skip to main content

orx_concurrent_iter/pullers/
item_puller.rs

1use crate::concurrent_iter::ConcurrentIter;
2
3/// A regular [`Iterator`] which is created from and linked to and
4/// pulls its elements from a [`ConcurrentIter`].
5///
6/// It can be created using the [`item_puller`] method of a concurrent iterator.
7///
8/// [`item_puller`]: crate::ConcurrentIter::item_puller
9///
10/// # Examples
11///
12/// The definition might sound a bit confusing.
13///
14/// The following example demonstrates how it works:
15///
16/// * We have a concurrent iterator `con_iter` over elements "0", "1", ..., "99".
17/// * We spawn two threads, say A and B.
18/// * Each thread creates an `ItemPuller`, named as `puller`, from the same `con_iter`.
19/// * The following is one possible sequence of this parallel execution.
20///   * Thread A pulls "0" and calls process("0").
21///   * Thread B pulls "1" and calls process("1"); it completes processing before A.
22///   * Thread B pulls "2" and calls process("2").
23///   * Thread A pulls "3" and calls process("3").
24///   * and so on until all 100 elements are processed by the two threads.
25/// * Notice that there is only one data source `con_iter`;
26///   both of the `puller`s are connected to the same concurrent iterator, and
27///   each element is processed only once.
28///
29/// ```
30/// use orx_concurrent_iter::*;
31///
32/// let num_threads = 2;
33/// let data: Vec<_> = (0..100).map(|x| x.to_string()).collect();
34/// let con_iter = data.con_iter();
35///
36/// let process = |_x: &String| { /* assume actual work */ };
37///
38/// std::thread::scope(|s| {
39///     for _ in 0..num_threads {
40///         s.spawn(|| {
41///             // puller implements Iterator
42///             let puller = con_iter.item_puller();
43///             for value in puller {
44///                 process(value);
45///             }
46///         });
47///     }
48/// });
49/// ```
50///
51/// This approach brings the convenience of regular Iterators to the concurrent code.
52/// The example above already demonstrates that we can now use a regular `for` loop as we are
53/// writing a sequential code, while the program is parallelized.
54///
55/// Actually, we could've written an equivalent of the above program by directly using the
56/// concurrent iterator's [`next`] method and a `while let` loop:
57///
58/// ```ignore
59/// // ...
60/// std::thread::scope(|s| {
61///     for _ in 0..num_threads {
62///         s.spawn(|| {
63///             while let Some(value) = con_iter.next() {
64///                 process(value);
65///             }
66///         });
67///     }
68/// });
69/// ```
70///
71/// [`next`]: crate::ConcurrentIter::next
72///
73/// However, the convenience of the pullers goes beyond the `for` loops.
74/// All beautiful ergonomic Iterator methods become available in concurrent programs.
75///
76/// The following example demonstrate a very simple yet efficient implementation of the
77/// parallelized version of the [`reduce`] operation.
78///
79/// Notice that the entire implementation is nothing but a chain of Iterator methods.
80///
81/// [`reduce`]: Iterator::reduce
82///
83/// ```
84/// use orx_concurrent_iter::*;
85///
86/// fn parallel_reduce<T, F>(
87///     num_threads: usize,
88///     con_iter: impl ConcurrentIter<Item = T>,
89///     reduce: F,
90/// ) -> Option<T>
91/// where
92///     T: Send,
93///     F: Fn(T, T) -> T + Sync,
94/// {
95///     std::thread::scope(|s| {
96///         (0..num_threads)
97///             .map(|_| s.spawn(|| con_iter.item_puller().reduce(&reduce))) // reduce inside each thread
98///             .filter_map(|x| x.join().unwrap()) // join threads
99///             .reduce(&reduce) // reduce thread results to final result
100///     })
101/// }
102///
103/// let sum = parallel_reduce(8, (0..0).into_con_iter(), |a, b| a + b);
104/// assert_eq!(sum, None);
105///
106/// let n = 10_000;
107/// let data: Vec<_> = (0..n).collect();
108/// let sum = parallel_reduce(8, data.con_iter().copied(), |a, b| a + b);
109/// assert_eq!(sum, Some(n * (n - 1) / 2));
110/// ```
111pub struct ItemPuller<'a, I> {
112    con_iter: &'a I,
113}
114
115impl<I: ConcurrentIter> ItemPuller<'_, I> {
116    /// Behaves exactly as `next` but additionally provides `thread_idx` to the iterator.
117    /// This information might be useful for certain concurrent iterators, such as the
118    /// [recursive concurrent iterator](https://crates.io/crates/orx-concurrent-recursive-iter).
119    ///
120    /// Assuming a program using `n` threads that accesses this iterator, `thread_idx` is
121    /// assumed to be the internal ordering within this pool of threads taking values in
122    /// `0..n`.
123    #[inline(always)]
124    pub fn next_by(&mut self, thread_idx: usize) -> Option<I::Item> {
125        self.con_iter.next_by(thread_idx)
126    }
127}
128
129impl<'i, I: ConcurrentIter> From<&'i I> for ItemPuller<'i, I> {
130    fn from(con_iter: &'i I) -> Self {
131        Self { con_iter }
132    }
133}
134
135impl<I: ConcurrentIter> Iterator for ItemPuller<'_, I> {
136    type Item = I::Item;
137
138    #[inline(always)]
139    fn next(&mut self) -> Option<Self::Item> {
140        self.con_iter.next()
141    }
142
143    fn size_hint(&self) -> (usize, Option<usize>) {
144        // lb: other threads might pull all of the elements, hence 0
145        // ub: we might pull all elements, hence ub(con_iter)
146        (0, self.con_iter.size_hint().1)
147    }
148
149    fn fold<B, F>(self, init: B, mut f: F) -> B
150    where
151        Self: Sized,
152        F: FnMut(B, Self::Item) -> B,
153    {
154        let mut acc = init;
155
156        while let Some(elem) = self.con_iter.next() {
157            acc = f(acc, elem);
158        }
159
160        acc
161    }
162
163    fn count(self) -> usize
164    where
165        Self: Sized,
166    {
167        self.fold(0, |count, _| count + 1)
168    }
169}