Awesome Reviewers

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

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.