Standard Library, Concurrency, and async: Rust Chapter Finale
After the journey of the first four articles—ownership and borrowing, type system, error handling, and pattern matching—we have now reached the Rust chapter finale, time to tie the scattered knowledge together.
This article focuses on Rust’s standard library concurrency tools and the async/await ecosystem. These are the most interesting comparative points between Rust and Go in the concurrency domain: Go makes concurrency look simple through goroutines and channels, while Rust makes concurrency safe and reliable through strict type systems and ownership constraints.
Finally, we will bridge to the next protagonist—Zig—at the end of the article.
std:🧵 Direct Mapping to OS Threads
Rust’s std::thread provides a 1:1 mapping to operating system threads, which is fundamentally different from Go’s M:N goroutine scheduling model.
Creating Threads and join
| |
Note for Go Developers:
- Go’s
go func()are goroutines (lightweight threads), Rust’sthread::spawnare operating system threads (each goroutine has ~2KB stack, each OS thread has ~2MB stack). - Go’s main function returning causes all goroutines to exit, Rust’s main thread ending does not automatically wait for child threads (need explicit
join). - Go’s scheduler is M:N, Rust’s is 1:1.
Closure Capture and move
| |
The move keyword forces transferring ownership to the new thread. This is different from Go’s closure capture—Go’s closures capture variables by pointer (shared), Rust’s closures capture by reference (borrowing) by default, requiring move to transfer ownership.
Message Passing: Rust’s Channel Implementation
Rust’s std::sync::mpsc provides multi-producer single-consumer channels, sharing the same design philosophy as Go’s channels—“don’t communicate by sharing memory, share memory by communicating.”
mpsc::channel Basic Usage
| |
Go Comparison:
- Go:
ch := make(chan string)/ch <- "hello"/val := <-ch - Rust:
(tx, rx) = mpsc::channel()/tx.send(val)/rx.recv() - Go’s channels can be read/written by multiple goroutines, Rust’s mpsc channels only support multiple senders and one receiver (mpsc = multi-producer, single-consumer).
- Go’s channels support buffering and close detection, Rust’s mpsc channels also support capacity setting (
mpsc::sync_channel(capacity)).
Multiple Producers
| |
Note for Go Developers: Go’s channels don’t need explicit cloning because channels themselves are reference types. Rust’s Sender needs explicit cloning to be used in multiple threads.
Shared State: Mutex and Arc
Besides message passing, Rust also supports a concurrent model through shared state—implementing thread-safe shared mutable state through Mutex and Arc.
Mutex Basic Usage
| |
Multi-thread Shared Mutex (Requires Arc)
| |
Arc (Atomic Reference Counting) provides thread-safe reference counting, allowing multiple threads to share the same ownership.
Go Comparison:
- Go doesn’t have explicit Mutex/Arc concepts—
sync.Mutexis a value type, used directly. - Go’s channels can pass pointers to implement shared state, Rust’s Arc is an explicit shared ownership mechanism.
- Go’s
mutex.Lock()/mutex.Unlock()requires manual calling defer to release, Rust’sMutexGuardis automatically released when scope ends (RAII).
RwLock: Read-Write Lock
| |
Go Equivalent: Go’s sync.RWMutex, but Rust’s RwLock is stricter—read locks and write locks cannot be held simultaneously.
async/await: Zero-Cost Abstraction Concurrency
Rust 1.39 introduced async/await, providing a zero-cost abstraction concurrent model based on Future. This is fundamentally different from Go’s goroutines—Go’s concurrency is implemented through runtime scheduler, Rust’s async is implemented through state machines.
async Function Basics
| |
Key Points:
async fnreturns aFuture, not the value directly..awaitactually executes this Future (not executed when calling the function).tokio::join!executes multiple Futures concurrently.
Note for Go Developers:
- Go’s goroutines execute immediately upon starting, Rust’s async functions don’t execute when called (return Future), need
.awaitto execute. - Go’s
go func()is true concurrency, Rust’s serial.awaitexecutes sequentially (need runtime for concurrency). - Go’s runtime schedules implicitly, Rust’s runtime needs explicit choice (tokio, async-std, smol).
Future Trait
The underlying async/await is the Future trait:
| |
Go Comparison:
- Go’s goroutines are directly managed by runtime, no need to manually implement Future-like mechanisms.
- Rust’s Future is more like a state machine, each
pollreturnsPendingorReady.
tokio runtime: Rust’s Concurrency Scheduler
tokio is the most popular async runtime in Rust, providing executors, timers, IO and other infrastructure.
Basic Usage
| |
Go Comparison:
- Go’s
go func()executes concurrently, tokio’stokio::spawnalso executes concurrently. - Go’s main function returning ends all goroutines, tokio’s main async function returning doesn’t end spawned tasks (need explicit waiting).
select!: Multi-way Selection
| |
Go Equivalent: Go’s select statement, but Rust’s select! is a macro, generating matching logic at compile time.
Comparison Summary: Rust vs Go Concurrency Models
| Feature | Go | Rust |
|---|---|---|
| Thread Model | M:N goroutine (lightweight) | 1:1 OS thread + async Future |
| Scheduler | Built-in runtime | Optional runtime (tokio, async-std) |
| Channel | Built-in, bidirectional many-to-many | std::sync::mpsc (unidirectional many-to-one) |
| Shared State | sync.Mutex (direct use) | Mutex<Arc |
| Error Handling | panic (exception-like) | Result<T, E> (type safe) |
| Compile-time Guarantees | Partial (race detector) | Complete (ownership + borrow checking) |
Go’s Advantages:
- Simpler concurrency model (goroutine + channel)
- Lower concurrency barrier (less mental burden)
- Built-in runtime, no selection needed
- High development efficiency
Rust’s Advantages:
- Complete concurrency safety (compile-time guarantees)
- Zero-cost abstractions (async/await introduces no runtime overhead)
- Finer-grained control (can choose different runtimes)
- Stronger type safety and error handling
Rust Chapter Summary
Rust’s concurrency model embodies the language’s core philosophy: safety is not compromised.
Through ownership and borrow checking, Rust eliminates data races at compile time; through Future and async/await, Rust provides zero-cost concurrent abstractions; through type systems and trait mechanisms, Rust makes concurrent code both safe and efficient.
But safety comes at a price—Rust’s concurrent code is more verbose than Go’s, with higher mental burden. Which one to choose depends on your project needs: pursue development efficiency and simplicity choose Go, pursue safety and zero-cost abstractions choose Rust.
Transition to Zig
From Go’s simple and efficient to Rust’s safe zero-cost, we’ve seen two different concurrency philosophies. Go makes concurrency a language feature, Rust makes concurrency a library and type system.
Zig takes a third path: neither built-in runtime nor forced abstraction.
In Zig, concurrency is explicit—you use std.Thread and it’s an OS thread, use std.Io.Group and it’s a task group, use Io.Evented and it’s event-driven. Zig won’t package concurrency as simple as go func(), but won’t require understanding complex concepts like Pin, Unpin, Waker like Rust.
In the next chapter, we will dive deep into Zig’s world and see how this language finds balance between “explicit first” and “zero runtime.”
Rust eliminates concurrency problems through strict type systems, Zig makes concurrent behavior visible through explicit APIs. Different philosophies, the same goal—writing safe and efficient concurrent code.
Next, let’s welcome Zig.