1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
//! Multithreading helpers.
//!
//! The main purpose of this module is to allow the number of worker threads used by each puzzle
//! solution to be controlled by a CLI argument.

use std::num::NonZeroUsize;
use std::sync::atomic::AtomicUsize;
use std::sync::atomic::Ordering::Relaxed;

static NUM_THREADS: AtomicUsize = AtomicUsize::new(0);

/// Get the number of worker threads to use.
///
/// Defaults to [`std::thread::available_parallelism`] unless set by [`set_thread_count`].
#[must_use]
pub fn get_thread_count() -> NonZeroUsize {
    if let Some(threads) = NonZeroUsize::new(NUM_THREADS.load(Relaxed)) {
        threads
    } else {
        let default = std::thread::available_parallelism().unwrap_or(NonZeroUsize::new(8).unwrap());
        match NUM_THREADS.compare_exchange(0, default.get(), Relaxed, Relaxed) {
            Ok(_) => default,
            Err(not_zero) => NonZeroUsize::new(not_zero).unwrap(),
        }
    }
}

/// Set the number of worker threads to use.
///
/// This will affect any future call to [`get_thread_count`].
pub fn set_thread_count(count: NonZeroUsize) {
    NUM_THREADS.store(count.get(), Relaxed);
}

/// Run a worker function concurrently using a pool of worker threads.
///
/// This is a wrapper around [`std::thread::scope`] for spawning a pool of identical worker threads.
///
/// The number of workers is controlled by [`get_thread_count`].
pub fn worker_pool(worker: impl Fn() + Copy + Send) {
    let threads = get_thread_count().get();
    if threads == 1 {
        worker();
    } else {
        std::thread::scope(|scope| {
            for _ in 0..threads {
                scope.spawn(worker);
            }
        });
    }
}