When concurrency involves shared state and/or synchronous callers (IPC handlers, UI main thread), follow these rules:
1) Keep global locks short
2) Decouple blocking IO from critical threads
terminal_write enqueues data; a per-session writer thread performs the blocking write_all.3) Make lifecycle updates race-safe
4) Use timeouts for cross-process probing
Example pattern (writer decoupling + safe cleanup):
use std::{collections::HashMap, sync::{Arc, Mutex, mpsc}, thread};
struct Session {
tx: mpsc::Sender<Vec<u8>>,
generation: u64,
}
type Sessions = Arc<Mutex<HashMap<String, Session>>>;
fn start_session(sessions: Sessions, id: String, generation: u64, writer: impl std::io::Write + Send + 'static) {
let (tx, rx) = mpsc::channel::<Vec<u8>>();
// Register session (under lock, non-blocking)
sessions.lock().unwrap().insert(id.clone(), Session { tx, generation });
// Worker thread performs blocking writes
thread::spawn(move || {
let mut w = writer;
for chunk in rx {
let _ = w.write_all(&chunk); // blocking, but not on the caller/IPC thread
}
// When tx is dropped, loop ends.
});
}
fn terminal_write(sessions: &Sessions, id: &str, data: &[u8]) {
// Lock only to clone/enqueue; do NOT block while holding it.
let tx = {
let map = sessions.lock().unwrap();
map.get(id).map(|s| s.tx.clone()).unwrap()
};
let _ = tx.send(data.to_vec());
}
Adopting this standard prevents common stalls (blocked IO freezing IPC/UI), and prevents subtle race bugs where background threads remove or mutate the wrong session entry after replacement.