use std::collections::VecDeque;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Condvar, Mutex, Weak};
use std::thread::JoinHandle;
use crate::graph::{GraphInner, NodeId};
struct Shared {
queue: Mutex<VecDeque<NodeId>>,
cv: Condvar,
stop: AtomicBool,
}
impl Shared {
fn take(&self) -> Option<NodeId> {
let mut q = self.queue.lock().unwrap_or_else(|e| e.into_inner());
loop {
if let Some(n) = q.pop_front() {
return Some(n);
}
if self.stop.load(Ordering::SeqCst) {
return None;
}
q = self.cv.wait(q).unwrap_or_else(|e| e.into_inner());
}
}
}
pub struct ThreadPool {
name: String,
num_threads: usize,
affinity: Vec<usize>,
priority: i32,
shared: Arc<Shared>,
threads: Mutex<Vec<JoinHandle<()>>>,
}
#[cfg(any(target_os = "linux", target_os = "android"))]
fn pin_current_thread_to(cpu: usize) {
extern "C" {
fn sched_setaffinity(pid: i32, cpusetsize: usize, mask: *const u64) -> i32;
}
const NBITS: usize = 1024; if cpu >= NBITS {
return;
}
let mut mask = [0u64; NBITS / 64];
mask[cpu / 64] |= 1u64 << (cpu % 64);
unsafe {
let _ = sched_setaffinity(0, std::mem::size_of_val(&mask), mask.as_ptr());
}
}
#[cfg(not(any(target_os = "linux", target_os = "android")))]
fn pin_current_thread_to(_cpu: usize) {}
#[cfg(any(target_os = "linux", target_os = "android"))]
fn set_current_thread_rt_priority(prio: i32) {
extern "C" {
fn sched_setscheduler(pid: i32, policy: i32, param: *const SchedParam) -> i32;
}
#[repr(C)]
struct SchedParam {
sched_priority: i32,
}
const SCHED_FIFO: i32 = 1;
let param = SchedParam {
sched_priority: prio.clamp(1, 99),
};
unsafe {
let _ = sched_setscheduler(0, SCHED_FIFO, ¶m);
}
}
#[cfg(any(target_os = "macos", target_os = "ios"))]
fn set_current_thread_rt_priority(prio: i32) {
extern "C" {
fn pthread_set_qos_class_self_np(qos_class: u32, relative_priority: i32) -> i32;
}
const QOS_CLASS_USER_INITIATED: u32 = 0x19;
const QOS_CLASS_USER_INTERACTIVE: u32 = 0x21;
let qos = if prio >= 90 {
QOS_CLASS_USER_INTERACTIVE
} else {
QOS_CLASS_USER_INITIATED
};
unsafe {
let _ = pthread_set_qos_class_self_np(qos, 0);
}
}
#[cfg(not(any(
target_os = "linux",
target_os = "android",
target_os = "macos",
target_os = "ios"
)))]
fn set_current_thread_rt_priority(_prio: i32) {}
impl ThreadPool {
pub fn new(name: &str, num_threads: usize, affinity: Vec<usize>, priority: i32) -> Self {
Self {
name: name.to_string(),
num_threads: num_threads.max(1),
affinity,
priority,
shared: Arc::new(Shared {
queue: Mutex::new(VecDeque::new()),
cv: Condvar::new(),
stop: AtomicBool::new(false),
}),
threads: Mutex::new(Vec::new()),
}
}
pub fn name(&self) -> &str {
&self.name
}
pub fn num_threads(&self) -> usize {
self.num_threads
}
pub fn affinity(&self) -> &[usize] {
&self.affinity
}
pub fn priority(&self) -> i32 {
self.priority
}
pub fn start(&self, graph: Weak<GraphInner>) {
let mut handles = self.threads.lock().unwrap_or_else(|e| e.into_inner());
if !handles.is_empty() {
return; }
for i in 0..self.num_threads {
let shared = self.shared.clone();
let weak = graph.clone();
let tname = format!("{}-{}", self.name, i);
let cpu = if self.affinity.is_empty() {
None
} else {
Some(self.affinity[i % self.affinity.len()])
};
let priority = self.priority;
let h = std::thread::Builder::new()
.name(tname)
.spawn(move || {
if let Some(c) = cpu {
pin_current_thread_to(c);
}
if priority > 0 {
set_current_thread_rt_priority(priority);
}
worker(shared, weak)
})
.expect("failed to create worker thread");
handles.push(h);
}
}
pub fn submit(&self, node: NodeId) -> bool {
let mut q = self.shared.queue.lock().unwrap_or_else(|e| e.into_inner());
if self.shared.stop.load(Ordering::SeqCst) {
return false;
}
q.push_back(node);
drop(q);
self.shared.cv.notify_one();
true
}
pub fn pending(&self) -> usize {
self.shared
.queue
.lock()
.unwrap_or_else(|e| e.into_inner())
.len()
}
pub fn shutdown(&self) {
{
let _q = self.shared.queue.lock().unwrap_or_else(|e| e.into_inner());
self.shared.stop.store(true, Ordering::SeqCst);
}
self.shared.cv.notify_all();
let handles: Vec<JoinHandle<()>> = {
let mut h = self.threads.lock().unwrap_or_else(|e| e.into_inner());
std::mem::take(&mut *h)
};
for h in handles {
let _ = h.join();
}
}
}
impl Drop for ThreadPool {
fn drop(&mut self) {
self.shutdown();
}
}
fn worker(shared: Arc<Shared>, graph: Weak<GraphInner>) {
while let Some(node) = shared.take() {
match graph.upgrade() {
Some(g) => g.run_node_on_worker(node),
None => break,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn zero_threads_becomes_one() {
let p = ThreadPool::new("t", 0, vec![], 0);
assert_eq!(
p.num_threads(),
1,
"a pool configured with zero threads is surely not intended"
);
}
#[test]
fn submit_before_start_is_queued() {
let p = ThreadPool::new("t", 2, vec![], 0);
assert!(p.submit(1));
assert_eq!(p.pending(), 1, "tasks should be queued before start");
}
#[test]
fn submit_after_shutdown_is_rejected() {
let p = ThreadPool::new("t", 1, vec![], 0);
p.shutdown();
assert!(
!p.submit(1),
"after shutdown, submit must explicitly reject so the caller can release the token"
);
}
#[test]
fn shutdown_is_idempotent_and_joins() {
let p = ThreadPool::new("t", 3, vec![], 0);
p.shutdown();
p.shutdown();
}
#[cfg(any(target_os = "linux", target_os = "android"))]
#[test]
fn affinity_actually_pins_worker_thread() {
use std::sync::mpsc;
extern "C" {
fn sched_getaffinity(pid: i32, cpusetsize: usize, mask: *mut u64) -> i32;
}
let ncpu = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1);
if ncpu < 2 {
return;
}
let (tx, rx) = mpsc::channel::<[u64; 16]>();
let handle = std::thread::spawn(move || {
super::pin_current_thread_to(1);
let mut mask = [0u64; 16];
let rc =
unsafe { sched_getaffinity(0, std::mem::size_of_val(&mask), mask.as_mut_ptr()) };
assert_eq!(rc, 0, "sched_getaffinity failed");
tx.send(mask).unwrap();
});
let mask = rx.recv().unwrap();
handle.join().unwrap();
assert_eq!(
mask[0],
1u64 << 1,
"worker must be pinned to exactly CPU 1, actual mask {:#b}",
mask[0]
);
}
#[cfg(any(target_os = "linux", target_os = "android"))]
#[test]
fn rt_priority_is_best_effort() {
extern "C" {
fn sched_getscheduler(pid: i32) -> i32;
}
const SCHED_FIFO: i32 = 1;
let handle = std::thread::spawn(|| {
super::set_current_thread_rt_priority(10);
unsafe { sched_getscheduler(0) }
});
let policy = handle.join().unwrap();
assert!(
policy == SCHED_FIFO || policy >= 0,
"setting priority must not leave the thread in an invalid scheduling state, actual policy={policy}"
);
}
}