Rust
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Ownership and Borrowing
-
Section 4: Structuring Data
-
Section 5: Collections and Error Handling
-
Section 6: Traits and Generics
-
Section 7: Concurrency
-
Section 8: Building for the Web
-
Section 9: Memory and Performance
-
Section 10: More Standard Library and Ecosystem
-
Section 11: Advanced Rust
-
Section 12: Rust for Systems and WebAssembly
-
Section 13: Tooling and Best Practices
-
Section 14: Data Structures and Algorithms in Rust
-
Section 15: Practical Projects
-
Section 16: Interview Practice
-
Section 17: std::collections In Depth
-
Section 18: std::io and std::fs In Depth
-
Section 19: std::net
-
Section 20: std::option and std::result In Depth
-
Section 21: std::iter In Depth
-
Section 22: std::sync In Depth
-
Section 23: std::string and std::str
-
Section 24: Cargo and Crates.io Ecosystem
-
Section 25: Popular Crates Ecosystem
-
Section 26: Rust Memory Model Deep Dive
-
Section 27: More Practice Exercises
-
Section 28: More Interview Practice
-
Section 29: Async Rust Deep Dive
-
Section 30: Tokio Ecosystem In Depth
-
Section 31: Error Handling Ecosystem Deep Dive
-
Section 32: Serde In Depth
-
Section 33: Web Development with Rust Deep Dive
-
Section 34: Database Access Ecosystem
-
Section 35: Rust for Embedded Systems Deep Dive
-
Section 36: Rust Macros In Depth
-
Section 37: Advanced Trait System
-
Section 38: Unsafe Rust In Depth
-
Section 39: Rust CLI Development
-
Section 40: Testing Ecosystem Deep Dive
-
Section 41: WebAssembly Deep Dive
-
Section 42: Rust Design Patterns
-
Section 43: More Data Structures in Rust
-
Section 44: Final Practice Projects
-
Section 45: Rust Performance Optimization
-
Section 46: Rust Ecosystem Tooling
-
Section 47: More Interview and Review
153: Tokio Runtime Configuration
Up until now, you've probably been using #[tokio::main] to kick off your async functions. It's convenient, and for 90% of apps, it's all you need. But as you move into production environments—especially when you're dealing with constrained resources or very specific performance targets—you'll find that the default "one thread per CPU core" strategy isn't always the right move.
Do I really need to stop using the #[tokio::main] macro?
You don't have to, but the macro is essentially just a wrapper that builds a multi-threaded runtime with default settings. If you need to tweak how the runtime behaves, you have to step away from the macro and use the tokio::runtime::Builder.
I usually switch to the manual Builder when I'm writing a library that shouldn't dictate the runtime to the end-user, or when I'm building a service where I need to strictly limit resource consumption to play nice with Kubernetes CPU limits. Here is the basic pattern for replacing the macro:
use tokio::runtime::Builder;
fn main() {
let rt = Builder::new_multi_thread()
.worker_threads(4) // Explicitly set the number of threads
.enable_all() // Necessary to enable both I/O and Time drivers
.build()
.unwrap();
rt.block_on(async {
println!("Hello from a custom runtime!");
});
}
How do I prevent the runtime from hogging every core on my machine?
By default, Tokio detects how many cores your machine has and spawns that many worker threads. If you're running a telemetry aggregator in a container restricted to 2 cores, but the underlying VM has 64, Tokio might try to spawn 64 threads. This leads to massive context-switching overhead and can actually tank your throughput.
You can use .worker_threads(n) to cap this. But there's a nuance here: this only affects the async worker threads. If you're using spawn_blocking for heavy disk I/O or synchronous database calls, those run on a separate pool of threads. If you find your app is stalling because too many blocking tasks are queued, you'll want to configure the blocking thread limit too:
let rt = Builder::new_multi_thread()
.worker_threads(2)
.max_blocking_threads(512) // Allow more threads for synchronous I/O
.enable_all()
.build()
.unwrap();
I've seen cases where increasing the blocking pool saved a service from death-spiraling during a database slowdown, while keeping the worker threads low kept the CPU cache happy.
When should I use a single-threaded runtime instead?
The current_thread runtime is often overlooked. It runs everything on the thread that called block_on. There's no work-stealing, no synchronization overhead between threads, and no need to wrap every single piece of state in an Arc<Mutex<T>> if you're using Rc and RefCell (though you still need to be careful with async boundaries).
You'll want this for:
- Small CLI tools where the overhead of spawning a thread pool is larger than the actual work.
- WASM targets, where multi-threading is either unsupported or requires complex setup.
- Highly specialized low-latency loops where you want total control over which core the code executes on.
let rt = Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
Just remember: if you use the current-thread runtime and you accidentally call a long-running synchronous function inside an async block, you've just frozen your entire application. There are no other workers to pick up the slack.
📋 Practical Task
Exercise: Tuning a Telemetry Collector's Worker Pool
You are building a telemetry collector that runs on a very small sidecar container. To ensure it doesn't interfere with the main application, you must strictly limit its resource usage. Your task is to replace the #[tokio::main] macro in the provided logic with a manual tokio::runtime::Builder configuration.
Requirements:
- The runtime must use the
multi_threadscheduler. - The number of worker threads must be exactly 2.
- The maximum number of blocking threads must be set to 100.
- Both the I/O and Time drivers must be enabled.
- The program should execute the
run_collectorasync function.
// Use this as your starting point
async fn run_collector() {
println!("Collector is running on a tuned runtime...");
}
fn main() {
// Your implementation here:
// Replace the macro logic with a Builder
}There are no comments for now.