# Understanding Async Concurrency vs Multithreading in Rust with Tokio

## Introduction

When working with async Rust, developers often confuse **concurrency** with **parallelism/multithreading**. This post demonstrates the crucial difference using Tokio, showing how tasks can run concurrently on a single thread versus being distributed across multiple threads.

**Source Code**: [https://github.com/Ashwin-3cS/concurrency-multithreading-tokio](https://github.com/Ashwin-3cS/concurrency-multithreading-tokio)

## Table of Contents

1. [Core Concepts](#core-concepts)
    
2. [Running the Examples](#running-the-examples)
    
3. [Code Walkthrough: Multithreading with `tokio::spawn`](#multithreading-with-spawn)
    
4. [Understanding the Runtime](#understanding-the-runtime)
    
5. [Concurrency Without Spawning](#concurrency-without-spawning)
    
6. [Key Takeaways](#key-takeaways)
    

## Core Concepts

Before diving into the code, let's clarify these terms:

* **Concurrency**: Multiple tasks making progress, but not necessarily at the same instant. Tasks can yield control to each other.
    
* **Parallelism/Multithreading**: Multiple tasks executing simultaneously on different CPU cores/threads.
    
* `tokio::spawn`: Creates a new task that can be scheduled on any available thread in the runtime.
    
* `async/await`: Enables cooperative multitasking where tasks can pause and resume.
    

## Running the Examples

Clone the repository and run the examples:

```bash
# Clone the repository
git clone https://github.com/Ashwin-3cS/concurrency-multithreading-tokio.git
cd concurrency-multithreading-tokio

# Install dependencies
cargo build

# Run the multithreading example (with spawn)
cargo run --example multithreaded_with_spawn

# Run the concurrent example (without spawn) 
cargo run --example concurrent_without_spawn

# Run the main example (defaults to multithreading demo)
cargo run

# Run with detailed thread information
RUST_LOG=debug cargo run --example multithreaded_with_spawn
```

## Code Walkthrough: Multithreading with `tokio::spawn`

Let's examine the main code that demonstrates multithreading:

```rust
use tokio::time::{Duration, sleep};
use std::thread;

#[tokio::main(flavor = "multi_thread", worker_threads = 2)]
async fn main() {
    let task_1 = tokio::task::spawn(async { 
        println!("task ONE started on {:?}", thread::current().id());
        sleep(Duration::from_secs(2)).await;
        println!("task ONE finished on {:?}", thread::current().id());
    });
    
    let t2 = tokio::task::spawn(async {
        println!("task TWO started on {:?}", thread::current().id());
        for i in 1..=5 {
            println!("task TWO step {} on {:?}", i, thread::current().id());
            sleep(Duration::from_millis(500)).await;
        }
        println!("task TWO finished on {:?}", thread::current().id());
    });
    
    let _ = tokio::join!(task_1, t2);
}
```

### Breaking Down Each Part

#### 1\. Runtime Configuration

```rust
#[tokio::main(flavor = "multi_thread", worker_threads = 2)]
```

* `flavor = "multi_thread"`: Configures Tokio to use a multi-threaded runtime
    
* `worker_threads = 2`: Creates exactly 2 worker threads to execute tasks
    
* This sets up a thread pool where spawned tasks can be distributed
    

#### 2\. Task Spawning

```rust
let task_1 = tokio::task::spawn(async { 
    // task code
});
```

* `tokio::task::spawn`: Submits the async block to the Tokio runtime
    
* The runtime decides which thread will execute this task
    
* Returns a `JoinHandle` that can be awaited to get the task's result
    

#### 3\. Thread ID Tracking

```rust
println!("task ONE started on {:?}", thread::current().id());
```

* `thread::current().id()`: Gets the OS thread ID where the code is currently executing
    
* This helps visualize which thread is running each task
    
* You'll likely see different thread IDs for different tasks
    

#### 4\. Async Sleep

```rust
sleep(Duration::from_secs(2)).await;
```

* **Why** `.await`?: The `await` keyword yields control back to the runtime
    
* While this task sleeps, the thread can execute other tasks
    
* This is **non-blocking** - the thread isn't idle, it can do other work
    

#### 5\. Task Coordination

```rust
let _ = tokio::join!(task_1, t2);
```

* `tokio::join!`: Waits for all specified tasks to complete
    
* Ensures both tasks finish before the program exits
    
* The `_` indicates we're not using the return values
    

## Understanding the Runtime

Here's how multithreading works in this example:

1. **Task Submission**: When you call `tokio::spawn`, you submit a task to the runtime's queue
    
2. **Thread Pool Distribution**: The runtime has 2 worker threads constantly checking for tasks
    
3. **Work Stealing**: If one thread is idle and another has queued tasks, work can be redistributed
    
4. **Concurrent Execution**: Both tasks can literally run at the same time on different CPU cores
    

### Expected Output Pattern

```rust
task ONE started on ThreadId(2)
task TWO started on ThreadId(3)
task TWO step 1 on ThreadId(3)
task TWO step 2 on ThreadId(3)
task TWO step 3 on ThreadId(3)
task TWO step 4 on ThreadId(3)
task ONE finished on ThreadId(2)
task TWO step 5 on ThreadId(3)
task TWO finished on ThreadId(3)
```

Notice how tasks run on different threads (ThreadId 2 and 3), enabling true parallelism.

## Concurrency Without Spawning

For comparison, here's how to achieve concurrency without `spawn`:

```rust
use tokio::time::{Duration, sleep};
use std::thread;

#[tokio::main(flavor = "current_thread")]
async fn main() {
    // Define async functions
    async fn task_one() {
        println!("task ONE started on {:?}", thread::current().id());
        sleep(Duration::from_secs(2)).await;
        println!("task ONE finished on {:?}", thread::current().id());
    }
    
    async fn task_two() {
        println!("task TWO started on {:?}", thread::current().id());
        for i in 1..=5 {
            println!("task TWO step {} on {:?}", i, thread::current().id());
            sleep(Duration::from_millis(500)).await;
        }
        println!("task TWO finished on {:?}", thread::current().id());
    }
    
    // Run concurrently on the same thread
    tokio::join!(task_one(), task_two());
}
```

### Key Differences:

* **No** `spawn`: Tasks are not submitted to a thread pool
    
* **Single thread**: All tasks run on the main thread
    
* **Cooperative**: Tasks yield to each other at await points
    
* **Still concurrent**: Tasks interleave execution, making progress "simultaneously"
    

## Key Takeaways

1. `tokio::spawn` enables multithreading: Tasks can run on different OS threads in parallel
    
2. **Without** `spawn`, you get concurrency: Tasks share the same thread but still make concurrent progress
    
3. `.await` is the yield point: This is where tasks can switch, enabling concurrency
    
4. **Thread pools distribute work**: The runtime manages which thread executes which task
    
5. **Choose based on needs**:
    
    * Use `spawn` for CPU-intensive tasks that benefit from parallelism
        
    * Use concurrent execution for I/O-bound tasks that mostly wait
        

## When to Use Each Approach

### Use `tokio::spawn` (Multithreading) when:

* You have CPU-intensive computations
    
* Tasks are independent and can truly run in parallel
    
* You want to utilize multiple CPU cores
    
* You need isolation between tasks
    

### Use Concurrent Execution (without spawn) when:

* Tasks are mostly I/O-bound (network, disk, etc.)
    
* You want simpler code without spawn overhead
    
* Tasks need to share data without synchronization
    
* You're fine with single-threaded execution
    

## Conclusion

Understanding the difference between concurrency and multithreading is crucial for writing efficient async Rust code. While `tokio::spawn` gives you true parallelism across threads, you can achieve impressive concurrency even on a single thread through async/await. Choose the approach that best fits your specific use case!
