Files
codex/prs/bolinfest/study/PR-2747-study.md
2025-09-02 15:17:45 -07:00

3.6 KiB
Raw Blame History

DOs

  • Move the sender: Move mpsc::Sender into the producer task so it drops when the task ends, allowing the receiver to finish.
use tokio::sync::mpsc;

let (tx, mut rx) = mpsc::channel::<String>(16);

let stdin_task = tokio::spawn(async move {
    let _ = tx.send("line".into()).await;
}); // tx drops here

let processor = tokio::spawn(async move {
    while let Some(msg) = rx.recv().await {
        // handle msg
    } // exits when all senders drop
});
  • Drop before join: If the parent holds an extra sender (e.g., for setup), drop it explicitly before awaiting joins.
let (tx, mut rx) = mpsc::channel::<()>(1);
let tx_parent = tx.clone();

let t = tokio::spawn(async move {
    let _ = tx.send(()).await;
});

// Critical: close the channel on the parent side
drop(tx_parent);

let _ = tokio::join!(t);
  • Rely on move capture: Let async move capture needed variables; avoid needless rebindings inside tokio::spawn.
let (tx, _rx) = tokio::sync::mpsc::channel::<String>(16);

let task = tokio::spawn(async move {
    let _ = tx.send("ok".into()).await; // use tx directly
});
  • Bound receiver loops: Use while let Some(...) to exit cleanly when the channel closes.
let processor = tokio::spawn(async move {
    while let Some(msg) = rx.recv().await {
        // process msg
    } // channel closed => loop ends
});
  • Clone intentionally: Only clone when you truly need multiple producers, and ensure each clone is dropped.
let (tx, mut rx) = tokio::sync::mpsc::channel::<String>(16);
let tx2 = tx.clone();

let a = tokio::spawn(async move { let _ = tx.send("a".into()).await; });
let b = tokio::spawn(async move { let _ = tx2.send("b".into()).await; });

let _ = tokio::join!(a, b);

while let Some(_msg) = rx.recv().await {}

DONTs

  • Clone inside the task needlessly: Dont create an extra Sender clone in a spawned task when the closure can move the original.
// Anti-pattern: leaves an extra Sender alive outside the task
let (tx, mut rx) = tokio::sync::mpsc::channel::<String>(16);

let stdin_task = tokio::spawn({
    let tx_clone = tx.clone(); // unnecessary clone
    async move {
        let _ = tx_clone.send("line".into()).await;
    }
});

// If `tx` remains alive here, `rx` may never see closure.
  • Keep spare senders across join: Dont hold a Sender in the parent scope while awaiting task completion.
let (tx, mut rx) = tokio::sync::mpsc::channel::<()>(1);
let tx_main = tx.clone();

let t = tokio::spawn(async move { let _ = tx.send(()).await; });

// Anti-pattern: join while `tx_main` is still alive
let _ = tokio::join!(t); // receiver may never finish
  • Self-assign in closures: Dont write let tx = tx; inside the tokio::spawn block; remove the line and rely on move capture.
// Anti-pattern
let task = tokio::spawn({
    let tx = tx; // redundant; just omit this line
    async move { /* use tx */ }
});
  • Create accidental long-lived owners: Dont store the sender in a struct or outer variable that outlives the worker tasks unless you explicitly manage its drop.
struct State { tx: tokio::sync::mpsc::Sender<String> }
let state = State { tx }; // long-lived owner
// … later …
let _ = tokio::join!(worker1, worker2); // may hang if `state.tx` is still alive
  • Assume receiver stops without closure: Dont use unbounded loops that ignore channel closure; always handle None from recv().
// Anti-pattern
loop {
    // `recv()` returns Option<T>; ignoring None risks hangs or errors
    if let Some(msg) = rx.recv().await {
        // process
    } else {
        break; // handle closure explicitly
    }
}