1use std::panic::{AssertUnwindSafe, catch_unwind};
4use std::sync::Arc;
5use std::sync::mpsc::{RecvTimeoutError, Sender, channel};
6use std::thread::JoinHandle;
7use std::time::Duration;
8
9use crate::exec::Ctx;
10use crate::{Gpu, GpuInfo};
11
12type Job = Box<dyn for<'c, 'g> FnOnce(&'c Ctx<'g>) + Send + 'static>;
14
15#[derive(Debug, Clone, PartialEq, Eq)]
17pub enum WaitError {
18 TimedOut,
20 Failed(String),
22}
23
24pub struct Worker {
26 tx: Option<Sender<Job>>,
27 thread: Option<JoinHandle<()>>,
28 info: GpuInfo,
29}
30
31impl Worker {
32 pub fn spawn(index: usize) -> Result<Worker, String> {
35 let (tx, rx) = channel::<Job>();
36 let (ready_tx, ready_rx) = channel();
37 let thread = std::thread::Builder::new()
38 .name("pagesift-gpu".into())
39 .spawn(move || {
40 let gpu = match Gpu::open_index(index).and_then(|g| g.info().map(|i| (g, i))) {
41 Ok((g, info)) => {
42 let _ = ready_tx.send(Ok(info));
44 g
45 }
46 Err(e) => {
47 let _ = ready_tx.send(Err(e));
48 return;
49 }
50 };
51 let ctx = Ctx::new(&gpu);
52 for job in rx {
53 job(&ctx);
54 }
55 })
56 .map_err(|e| format!("failed to start the GPU worker thread: {e}"))?;
57 let info = ready_rx
58 .recv()
59 .map_err(|_| "the GPU worker thread exited during startup".to_owned())??;
60 Ok(Worker {
61 tx: Some(tx),
62 thread: Some(thread),
63 info,
64 })
65 }
66
67 pub fn info(&self) -> &GpuInfo {
69 &self.info
70 }
71
72 pub fn call<R: Send>(
74 &self,
75 f: impl for<'c, 'g> FnOnce(&'c Ctx<'g>) -> R + Send,
76 ) -> Result<R, String> {
77 let (tx, rx) = channel();
78 let job = move |ctx: &Ctx<'_>| {
79 let _ = tx.send(catch_unwind(AssertUnwindSafe(|| f(ctx))));
81 };
82 let boxed: Box<dyn for<'c, 'g> FnOnce(&'c Ctx<'g>) + Send + '_> = Box::new(job);
83 let boxed: Job = unsafe { std::mem::transmute(boxed) };
86 let tx = self.tx.as_ref().expect("工人只在 Drop 里关闭");
87 tx.send(boxed)
88 .map_err(|_| "the GPU worker thread has exited".to_owned())?;
89 match rx.recv() {
90 Ok(Ok(r)) => Ok(r),
91 Ok(Err(_)) => Err("a job on the GPU worker panicked".into()),
92 Err(_) => Err("the GPU worker thread has exited".into()),
93 }
94 }
95}
96
97impl Worker {
98 pub fn call_timeout<R: Send + 'static>(
100 &self,
101 f: impl for<'c, 'g> FnOnce(&'c Ctx<'g>) -> R + Send + 'static,
102 wait: Duration,
103 ) -> Result<R, WaitError> {
104 let (tx, rx) = channel();
105 let job: Job = Box::new(move |ctx: &Ctx<'_>| {
106 let _ = tx.send(catch_unwind(AssertUnwindSafe(|| f(ctx))));
108 });
109 let tx = self.tx.as_ref().expect("工人只在 Drop 里关闭");
110 tx.send(job)
111 .map_err(|_| WaitError::Failed("the GPU worker thread has exited".into()))?;
112 match rx.recv_timeout(wait) {
113 Ok(Ok(r)) => Ok(r),
114 Ok(Err(_)) => Err(WaitError::Failed("a job on the GPU worker panicked".into())),
115 Err(RecvTimeoutError::Timeout) => Err(WaitError::TimedOut),
116 Err(RecvTimeoutError::Disconnected) => {
117 Err(WaitError::Failed("the GPU worker thread has exited".into()))
118 }
119 }
120 }
121}
122
123impl Drop for Worker {
124 fn drop(&mut self) {
125 drop(self.tx.take());
126 if let Some(t) = self.thread.take() {
127 let _ = t.join();
129 }
130 }
131}
132
133pub type SharedWorker = Arc<Worker>;
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139
140 #[test]
142 fn call_timeout_walks_away_from_a_slow_job() {
143 let Ok(w) = Worker::spawn(0) else {
144 eprintln!("没有能用的 N 卡,跳过");
145 return;
146 };
147 let slow = w.call_timeout(
148 |_: &Ctx<'_>| std::thread::sleep(Duration::from_millis(200)),
149 Duration::from_millis(1),
150 );
151 assert_eq!(slow, Err(WaitError::TimedOut));
152 let next = w.call_timeout(|_: &Ctx<'_>| 7, Duration::from_secs(60));
153 assert_eq!(next, Ok(7));
154 }
155}