Message Passing

Sharing Senders

In std::sync::mpsc, both Sender<T> and SyncSender<T> can be shared across producer threads in two ways:

  1. By cloning (tx.clone()): Required when spawning 'static threads with std::thread::spawn so each producer owns its own transmitter handle and the channel stays open until the last producer finishes.
  2. By shared reference (&tx): Enabled because Sender<T>: Sync and SyncSender<T>: Sync (whenever T: Send), allowing scoped threads inside std::thread::scope to send messages through a single borrowed &Sender<T> without cloning!

Comparing Cloned Sender<T> Handles vs. Borrowed &Sender<T>

Method 1: Cloned Senders (`tx.clone()` for `thread::spawn` `'static` Threads)
  Worker 1 owns `Sender<T>` clone  ---\
  Worker 2 owns `Sender<T>` clone  ----+--> Channel stays open until ALL clones drop!

Method 2: Borrowed Sender (`&tx` inside `thread::scope`)
  Worker 1 borrows `&Sender<T>`    ---\
  Worker 2 borrows `&Sender<T>`    ----+--> Zero counter increments; `tx` dropped after scope!
Sharing Approach Thread Type Reference-Count Overhead Automatic Disconnection When Workers Exit?
tx.clone() moved into each worker thread::spawn or thread::scope Atomic increment on clone, decrement on drop Yes, provided the parent thread drops its original tx!
&tx borrowed by each worker thread::scope Zero (&Sender<T> is a plain pointer copy) Parent drops tx after thread::scope finishes (or reads rx after scope)

Embedding a Sender<T> Inside a Cloneable Service Handle

In actor and background-service architectures, a background thread owns the Receiver<Command> while callers hold a lightweight #[derive(Clone)] client struct wrapping Sender<Command>:

use std::sync::mpsc::{self, Sender};
use std::thread::{self, JoinHandle};

enum AuditCommand {
    Record { user: String, action: String },
    FlushAndExit,
}

#[derive(Clone)]
pub struct AuditClient {
    tx: Sender<AuditCommand>,
}

impl AuditClient {
    pub fn record(&self, user: &str, action: &str) {
        let _ = self.tx.send(AuditCommand::Record {
            user: user.to_string(),
            action: action.to_string(),
        });
    }

    pub fn shutdown(&self) {
        let _ = self.tx.send(AuditCommand::FlushAndExit);
    }
}

fn spawn_audit_actor() -> (AuditClient, JoinHandle<Vec<String>>) {
    let (tx, rx) = mpsc::channel::<AuditCommand>();
    let handle = thread::spawn(move || {
        let mut logs = Vec::new();
        for cmd in rx {
            match cmd {
                AuditCommand::Record { user, action } => {
                    logs.push(format!("{user}:{action}"));
                }
                AuditCommand::FlushAndExit => break,
            }
        }
        logs
    });

    (AuditClient { tx }, handle)
}

fn main() {
    let (client, actor_handle) = spawn_audit_actor();

    thread::scope(|s| {
        for (user, action) in [("alice", "login"), ("bob", "upload")] {
            // Clone the `AuditClient` handle (which clones the inner `Sender`):
            let c = client.clone();
            s.spawn(move || {
                c.record(user, action);
            });
        }
    });

    client.shutdown();
    let mut recorded = actor_handle.join().unwrap();
    recorded.sort();
    println!("actor collected logs: {:?}", recorded);
}
actor collected logs: ["alice:login", "bob:upload"]