Skip to content
rayyildiz
← Back
rust

Async pattern in Rust and CSP

Async in Rust is one of those topics people argue about. Fans say it’s a big win for performance and scalability, especially when you’re I/O-bound or juggling lots of connections. Critics say it adds complexity and makes the code harder to follow. To get why both sides have a point, it helps to compare async with the Communicating Sequential Processes (CSP) model, which in Rust shows up as channels.

Understanding Asynchronous Programming

Async lets a program kick off a long-running task and get on with other work instead of sitting there waiting for it to finish. That’s a big deal in networked apps, where I/O can stall you for a while. In Rust, you mark a function with async and it returns a Future, which stands for a value that’ll be ready later. An executor in the runtime polls those Futures and drives them to completion.

Example

The standard library gives you the core traits and types for async, but it does not ship a runtime to actually run your Futures. For that you pull in a crate like tokio, async-std, or smol.

async fn fetch_data(url: &str) -> Result<String, &'static str> {
    println!("Fetching data from: {}", url);
    
    // business logic
    
    Ok(String::from(r#"{"name":"rayyildiz","bio":"just for fun"}"#))
}

#[tokio::main]
async fn main() {
    match fetch_data("https://api.rayyildiz.com").await {
        Ok(data) => println!("Received data: {}", data),
        Err(e) => println!("An error occurred: {}", e),
    }
}

You need to add tokio in your Cargo.toml to run this example.

[dependencies]
tokio = { version = "1.0", features = ["full"] }

Pros of Async in Rust:

Non-blocking I/O: Async allows for non-blocking I/O operations, enabling the handling of thousands of connections simultaneously without the overhead of threads.

Improved Performance: By avoiding unnecessary waits, async can significantly improve the performance of I/O-bound applications.

Resource Efficiency: Async reduces the need for threads, which are more expensive in terms of system resources.

Cons of Async in Rust:

Learning Curve: The async/await syntax and the concept of futures can be challenging for newcomers.

Complexity: Error handling, lifetime management, and task coordination can introduce complexity, making code harder to understand and maintain.

Compatibility: Not all libraries are async-aware, potentially leading to blocking calls that can negate the benefits of async programming.

Channels in Rust

CSP takes a different tack: concurrent processes talk to each other over channels instead of sharing memory. In Rust you get std::sync::mpsc (multi-producer, single-consumer) for the synchronous case and tokio::sync::mpsc for the async one.

Example

Here’s a small example using a std::sync::mpsc channel to pass values between threads. A spawned thread sends numbers across the channel, and the main thread adds them up and prints the total, which here is the sum of the first 10,000,000 positive integers.

use std::sync::mpsc;
use std::thread;

fn main() {
    let (tx, rx) = mpsc::channel();

    thread::spawn(move || {
        for i in 1..10_000_001 {
            tx.send(i as u64).unwrap();
        }
    });

    let mut sum = 0;
    for a in rx {
        sum += a;
    }
    println!("sum :{}", sum);
}

Pros of CSP in Rust:

Simplicity: CSP can be easier to reason about, as each process has its own state and communicates through well-defined channels.

Safety: Rust’s type system and ownership model ensure safe concurrent access to resources without data races.

Flexibility: Channels can be used in both synchronous and asynchronous contexts, making them versatile for different concurrency models.

Cons of CSP in Rust:

Overhead: The creation and management of channels and messages can introduce overhead, especially if not used judiciously.

Limited Scalability: For some highly concurrent applications, the overhead of message passing can become a bottleneck compared to non-blocking async operations.

Conclusion

Both async and CSP have a place in Rust. The “is async good or bad” question is the wrong one. It’s really about knowing the trade-offs and picking what fits the job in front of you. Rust’s type system and concurrency tools have your back either way, and plenty of real programs end up using both.

  • Use async for high-concurrency, I/O-bound applications where non-blocking I/O and resource efficiency are critical.

  • Consider CSP for applications where the logic naturally fits into distinct processes communicating through channels, and where the overhead of channels is justified by the benefits of easier reasoning and code safety.