This episode breaks down the async/await model and the tokio runtime, task spawning and channels with tokio::sync, thread safety through the Send and Sync traits, and the actor-like pattern for managing state shared between tasks.

The server in episode 10 could already handle many connections, but we have not broken down how that is possible. The answer is in episode 12: concurrency. Rust has one of the safest concurrency models in the world — its safety is guaranteed by the type system, not just by convention.
This episode breaks down async/await with the tokio runtime, task creation, communication between tasks with channels, thread safety through the Send and Sync traits, and the actor-like pattern for shared state. After this episode, you will be able to process many jobs concurrently without data races.
Operations such as waiting for the network or disk do not need CPU. Async allows a single thread to handle thousands of operations that are waiting: while one task waits, other tasks use the CPU. Tokio is the runtime that runs this model; add it with cargo add tokio --features full.
cat > src/main.rs <<'EOF'
async fn kerja_panjang(n: u64) -> u64 {
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
n * n
}
#[tokio::main]
async fn main() {
let mulai = std::time::Instant::now();
let a = tokio::spawn(kerja_panjang(2));
let b = tokio::spawn(kerja_panjang(3));
println!("hasil: {} dan {}", a.await.unwrap(), b.await.unwrap());
println!("waktu: {:?}", mulai.elapsed());
}
EOF
cargo runtokio::spawn runs tasks concurrently. Two tasks that each sleep 100 ms finish in ~100 ms, not 200 ms — because sleep does not block the thread. .await waits for a task's result without stopping the runtime. Tokio tasks are scheduled by a multi-threaded executor, so thousands of tasks can live in one process, and the JoinHandle from spawn carries a Result — an error in a task does not silently bring down the program.
A channel connects producers and consumers. mpsc (multi-producer, single-consumer) is the most common:
cat > src/main.rs <<'EOF'
use tokio::sync::mpsc;
#[tokio::main]
async fn main() {
let (tx, mut rx) = mpsc::channel(16);
for i in 0..3 {
let tx = tx.clone();
tokio::spawn(async move {
let _ = tx.send(format!("pesan {}", i)).await;
});
}
drop(tx);
while let Some(pesan) = rx.recv().await {
println!("terima: {}", pesan);
}
}
EOF
cargo runmpsc::channel(16) creates a channel with a buffer of 16. Each producer clones its sender, and rx.recv().await waits for the next message. drop(tx) closes the channel so the loop ends. This pattern routes results from many tasks into a single consumer.
Besides mpsc, tokio provides watch for state followed by many tasks and broadcast for sending to all subscribers. tokio::sync::watch is used, for example, to distribute changing configuration status, while broadcast delivers events to many receivers.
Two traits determine whether a value can safely be moved or shared between threads:
Send: the value can safely be moved to another thread.Sync: the value can safely be referenced from many threads at once.The compiler enforces them automatically based on the contents of the type. This is why Rust prevents data races: code that moves an unsafe value to another thread is rejected at compile time.
To share mutable state between tasks, wrap it in a Mutex inside an Arc (shared ownership):
cat > src/main.rs <<'EOF'
use std::sync::Arc;
use tokio::sync::Mutex;
#[tokio::main]
async fn main() {
let counter = Arc::new(Mutex::new(0u32));
let mut handles = Vec::new();
for _ in 0..4 {
let counter = counter.clone();
handles.push(tokio::spawn(async move {
let mut nilai = counter.lock().await;
*nilai += 1;
}));
}
for h in handles {
h.await.unwrap();
}
println!("nilai akhir: {}", *counter.lock().await);
}
EOF
cargo runArc lets many tasks hold a reference to the same data, and Mutex ensures only one task modifies it at a time. In tokio, use tokio::sync::Mutex when the guard crosses an .await point, and std::sync::Mutex for short critical sections.
The actor pattern: state is owned exclusively by one task, and other tasks interact through channels. No mutex is needed because the state is only touched by its owner.
cat > src/main.rs <<'EOF'
use tokio::sync::{mpsc, oneshot};
enum Perintah {
Tambah(u32, oneshot::Sender<u32>),
}
async fn actor(mut rx: mpsc::Receiver<Perintah>) {
let mut nilai = 0;
while let Some(perintah) = rx.recv().await {
match perintah {
Perintah::Tambah(n, reply) => {
nilai += n;
let _ = reply.send(nilai);
}
}
}
}
#[tokio::main]
async fn main() {
let (tx, rx) = mpsc::channel(16);
tokio::spawn(actor(rx));
let (reply, terima) = oneshot::channel();
let _ = tx.send(Perintah::Tambah(5, reply)).await;
println!("nilai: {}", terima.await.unwrap());
}
EOF
cargo runThe actor task holds nilai as private state. Calls are sent through mpsc, answers come back through oneshot. Because the state is never shared, safety is guaranteed — this is the actor-like pattern widely used in message-passing architectures, and its roots are visible in frameworks such as actix.
A concise guideline: for operations waiting on I/O, use tokio async tasks; for CPU-bound work, use threads (std::thread) or rayon; for small shared state, Arc<Mutex>; for large or frequently accessed state, consider the actor pattern; and to route results between tasks, use channels. These choices determine both the performance and the correctness of your program.
Key takeaways:
tokio::spawn runs tasks; .await waits without blocking the runtime.mpsc, watch, and broadcast connect tasks with different patterns.Send and Sync are guaranteed by the compiler; data races are rejected at compile time.In the next episode 13 we will discuss performance tuning and profiling — profiling a Rust application with perf, tokio-console, and cargo flamegraph, optimizing memory and CPU with zero-cost abstractions and iterators, as well as handling I/O bottlenecks, allocations, and lock contention.