use std::panic::{AssertUnwindSafe, catch_unwind};
use std::sync::Arc;
use std::sync::mpsc::{RecvTimeoutError, Sender, channel};
use std::thread::JoinHandle;
use std::time::Duration;
use crate::exec::Ctx;
use crate::{Gpu, GpuInfo};
type Job = Box<dyn for<'c, 'g> FnOnce(&'c Ctx<'g>) + Send + 'static>;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WaitError {
TimedOut,
Failed(String),
}
pub struct Worker {
tx: Option<Sender<Job>>,
thread: Option<JoinHandle<()>>,
info: GpuInfo,
}
impl Worker {
pub fn spawn(index: usize) -> Result<Worker, String> {
let (tx, rx) = channel::<Job>();
let (ready_tx, ready_rx) = channel();
let thread = std::thread::Builder::new()
.name("pagesift-gpu".into())
.spawn(move || {
let gpu = match Gpu::open_index(index).and_then(|g| g.info().map(|i| (g, i))) {
Ok((g, info)) => {
let _ = ready_tx.send(Ok(info));
g
}
Err(e) => {
let _ = ready_tx.send(Err(e));
return;
}
};
let ctx = Ctx::new(&gpu);
for job in rx {
job(&ctx);
}
})
.map_err(|e| format!("failed to start the GPU worker thread: {e}"))?;
let info = ready_rx
.recv()
.map_err(|_| "the GPU worker thread exited during startup".to_owned())??;
Ok(Worker {
tx: Some(tx),
thread: Some(thread),
info,
})
}
pub fn info(&self) -> &GpuInfo {
&self.info
}
pub fn call<R: Send>(
&self,
f: impl for<'c, 'g> FnOnce(&'c Ctx<'g>) -> R + Send,
) -> Result<R, String> {
let (tx, rx) = channel();
let job = move |ctx: &Ctx<'_>| {
let _ = tx.send(catch_unwind(AssertUnwindSafe(|| f(ctx))));
};
let boxed: Box<dyn for<'c, 'g> FnOnce(&'c Ctx<'g>) + Send + '_> = Box::new(job);
let boxed: Job = unsafe { std::mem::transmute(boxed) };
let tx = self.tx.as_ref().expect("工人只在 Drop 里关闭");
tx.send(boxed)
.map_err(|_| "the GPU worker thread has exited".to_owned())?;
match rx.recv() {
Ok(Ok(r)) => Ok(r),
Ok(Err(_)) => Err("a job on the GPU worker panicked".into()),
Err(_) => Err("the GPU worker thread has exited".into()),
}
}
}
impl Worker {
pub fn call_timeout<R: Send + 'static>(
&self,
f: impl for<'c, 'g> FnOnce(&'c Ctx<'g>) -> R + Send + 'static,
wait: Duration,
) -> Result<R, WaitError> {
let (tx, rx) = channel();
let job: Job = Box::new(move |ctx: &Ctx<'_>| {
let _ = tx.send(catch_unwind(AssertUnwindSafe(|| f(ctx))));
});
let tx = self.tx.as_ref().expect("工人只在 Drop 里关闭");
tx.send(job)
.map_err(|_| WaitError::Failed("the GPU worker thread has exited".into()))?;
match rx.recv_timeout(wait) {
Ok(Ok(r)) => Ok(r),
Ok(Err(_)) => Err(WaitError::Failed("a job on the GPU worker panicked".into())),
Err(RecvTimeoutError::Timeout) => Err(WaitError::TimedOut),
Err(RecvTimeoutError::Disconnected) => {
Err(WaitError::Failed("the GPU worker thread has exited".into()))
}
}
}
}
impl Drop for Worker {
fn drop(&mut self) {
drop(self.tx.take());
if let Some(t) = self.thread.take() {
let _ = t.join();
}
}
}
pub type SharedWorker = Arc<Worker>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn call_timeout_walks_away_from_a_slow_job() {
let Ok(w) = Worker::spawn(0) else {
eprintln!("没有能用的 N 卡,跳过");
return;
};
let slow = w.call_timeout(
|_: &Ctx<'_>| std::thread::sleep(Duration::from_millis(200)),
Duration::from_millis(1),
);
assert_eq!(slow, Err(WaitError::TimedOut));
let next = w.call_timeout(|_: &Ctx<'_>| 7, Duration::from_secs(60));
assert_eq!(next, Ok(7));
}
}