Async/Sync & Yield in Tokio
Amit Nadiger
Polyglot(Rust??, C++ 11,14,17,20, C, Kotlin, Java) Android TV, Cas, Blockchain, Polkadot, UTXO, Substrate, Wasm, Proxy-wasm,AndroidTV, Dvb, STB, Linux, Engineering management.
In the world of Rust and asynchronous programming, the Tokio crate stands as a powerful tool. Tokio enables developers to build highly concurrent and efficient applications, whether they involve I/O operations, network communication, or other asynchronous tasks. One of the distinguishing features of Tokio is its ability to seamlessly combine both synchronous (blocking) and asynchronous code.
The Tokio crate in Rust provides a versatile platform for building both synchronous and asynchronous applications. Whether you need to integrate blocking code into an asynchronous application or build a highly concurrent system, Tokio's features, including tokio::task::spawn_blocking and tokio::spawn, allow you to achieve your goals while maintaining the safety and performance characteristics that Rust is known for. By effectively leveraging both approaches, you can build robust and efficient applications that meet your specific requirements.
So there are 2 types of tasks can be created using tokio
Let's study each of them in detail:
Detached Tasks:
When you use tokio::spawn, you are creating a detached task. A detached task is one that is started asynchronously and runs independently of the current task, often referred to as the "parent" task. The detached task goes into the task queue managed by the Tokio runtime, and it executes concurrently with the other tasks in the queue.
Here's what happens when you spawn a detached task:
Detached tasks are ideal for scenarios where you want to perform work concurrently without waiting for the results immediately. For example, when building a web server, you might spawn a detached task to handle each incoming HTTP request, allowing multiple requests to be processed at the same time.
Here's a simplified example of using to launch detached tasks: tokio::spawn
const cntr: i8 = 5;
async fn chant_HareRam() {
for i in 0..cntr {
println!("HareRama {i}");
}
}
async fn chant_HareKrishana() {
for i in 0..cntr {
println!("HareKrishana {i}");
}
}
async fn chant_JaiBajrangbali() {
for i in 0..cntr {
println!("JaiBajrangBali {i}");
}
}
#[tokio::main]
async fn main() {
let task = tokio::task::spawn(async {
for i in 0..cntr {
println!("Om NamahShivaya {i}");
}
});
let _ = tokio::join!(
tokio::spawn(chant_HareRam()),
tokio::spawn(chant_HareKrishana()),
tokio::spawn(chant_JaiBajrangbali()),
task,
);
}
/*
Op =>
Om NamahShivaya 0
HareKrishana 0
HareKrishana 1
Om NamahShivaya 1
JaiBajrangBali 0
Om NamahShivaya 2
HareRama 0
HareKrishana 2
HareKrishana 3
HareKrishana 4
HareRama 1
HareRama 2
HareRama 3
Om NamahShivaya 3
Om NamahShivaya 4
JaiBajrangBali 1
JaiBajrangBali 2
JaiBajrangBali 3
JaiBajrangBali 4
HareRama 4
*/
In the above example, and are detached tasks that run concurrently with the main task. They are added to the Tokio task queue, and the Tokio runtime manages their execution.
Example 2:
In below example I am not using the join and also I am making the main task also executing concurrently the detached tasks.
const cntr: i8 = 5;
async fn chant_HareRam() {
for i in 0..cntr {
println!("HareRama {i}");
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}
}
async fn chant_HareKrishana() {
for i in 0..cntr {
println!("HareKrishana {i}");
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}
}
async fn chant_JaiBajrangbali() {
for i in 0..cntr {
println!("JaiBajrangBali {i}");
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}
}
#[tokio::main]
async fn main() {
let task = tokio::task::spawn(async {
for i in 0..cntr {
println!("Om NamahShivaya {i}");
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}
});
// Spawn two detached tasks
tokio::spawn(chant_HareRam());
tokio::spawn(chant_HareKrishana());
tokio::spawn(chant_JaiBajrangbali());
task;
// Main task continues executing concurrently with t1 and t2
for k in 0..5 {
println!("Shree Ganeshaya Namah {}", k);
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}
}
/*
Running `/home/amit/OmPracticeRust/AsyncExperements/Ardan-1HourAsync/target/debug/tokio_spawn`
Shree Ganeshaya Namah 0
JaiBajrangBali 0
HareKrishana 0
HareRama 0
Om NamahShivaya 0
Shree Ganeshaya Namah 1
HareKrishana 1
Om NamahShivaya 1
JaiBajrangBali 1
HareRama 1
Shree Ganeshaya Namah 2
Om NamahShivaya 2
HareRama 2
HareKrishana 2
JaiBajrangBali 2
Shree Ganeshaya Namah 3
JaiBajrangBali 3
HareRama 3
HareKrishana 3
Om NamahShivaya 3
Shree Ganeshaya Namah 4
HareRama 4
Om NamahShivaya 4
JaiBajrangBali 4
HareKrishana 4
*/
Detached tasks are a fundamental building block for writing highly concurrent and efficient asynchronous Rust applications with Tokio. They enable you to make the most of Rust's asynchronous capabilities while keeping your code responsive and scalable.
Non-Detached Tasks :
In Tokio, tasks created using tokio::task::spawn_blocking() are always detached by default. However, if we want to create non-detached tasks, we can use the function, which is designed for running synchronous or blocking code in a separate thread. Here's a complete example of using to launch non-detached
tokio::task::spawn_blocking is a function in Tokio specifically designed for running synchronous or blocking code in a separate thread. It is used when you have code that might block, like CPU-bound computations or code that relies on synchronous I/O operations (e.g., blocking file I/O, database queries, or interactions with synchronous libraries).
Key Characteristics:
Suitable Scenarios:
Use tokio::task::spawn_blocking in the following scenarios:
use tokio::time::Duration;
use tokio::task;
async fn async_task() {
for i in 0..5 {
println!("Async Task: Jai Shree Ram - Count {}", i);
tokio::time::sleep(Duration::from_secs(1)).await;
}
}
fn blocking_task() {
for j in 0..5 {
println!("Blocking Task:Jai Bajrang Bali - Count {}", j);
std::thread::sleep(Duration::from_secs(1))
}
}
#[tokio::main]
async fn main() {
// Spawn an asynchronous task (detached)
let async_handle = tokio::spawn(async_task());
// Spawn a non-detached task using spawn_blocking
let blocking_handle = tokio::task::spawn_blocking(|| {
blocking_task();
});
// Main task continues executing concurrently with async_handle and blocking_handle
for k in 0..5 {
println!("Main Task: Count {}", k);
tokio::time::sleep(Duration::from_secs(1)).await;
}
// Wait for the async_task to complete (optional)
async_handle.await.expect("Async Task panicked");
// The blocking_task automatically runs to completion
println!("All tasks have completed.");
}
/*
Running `target/debug/task_blocking_demo`
Async Task: Jai Shree Ram - Count 0
Main Task: Count 0
Blocking Task:Jai Bajrang Bali - Count 0
Blocking Task:Jai Bajrang Bali - Count 1
Async Task: Jai Shree Ram - Count 1
Main Task: Count 1
Blocking Task:Jai Bajrang Bali - Count 2
Async Task: Jai Shree Ram - Count 2
Main Task: Count 2
Blocking Task:Jai Bajrang Bali - Count 3
Async Task: Jai Shree Ram - Count 3
Main Task: Count 3
Blocking Task:Jai Bajrang Bali - Count 4
Async Task: Jai Shree Ram - Count 4
Main Task: Count 4
All tasks have completed.
*/
领英推荐
After going through all of the above about synchronous apis of tokio(tokio::spawn) , we still have below questions :
Question1 : Why should I use tokio::spawn() for spawning the synchronously when the main thread (main function) is by default synchronous ?
Question2 : Why should I use std::thread::spawn() for spawning the synchronously when I can create the seperate thread using std::thread::spawn() is also by default synchronous?
Answer both of the above question is almost same , but still adress both one by one.
Question1: Why should I use tokio::spawn () for spawning the synchronously when the main thread (main function) is by default synchronous?
The key difference between using tokio::task::spawn_blocking and writing synchronous code without Tokio is the context in which our code runs and the way it interacts with Tokio's asynchronous runtime.
Question2 : Why should I use tokio::task::spawn_blocking() for spawning the synchronously when I can create the seperate thread using std::thread::spawn() is also by default synchronous?
Using tokio::task::spawn_blocking and spawning a separate thread with std::thread::spawn() are two different approaches to executing synchronous code concurrently. Let's explore the differences between the two:
Let me list the key differences:
Which approach to use depends on the specific requirements of the project:
Ultimately, the choice between these approaches depends on the project's requirements, concurrency needs, and whether we are working within the context of an asynchronous runtime.
Yield:
The use of yielding, task scheduling, and the decision to use blocking or non-blocking operations depend on the specific requirements of your application. Yielding is a key mechanism in asynchronous programming that ensures responsiveness and efficient resource utilization, while blocking is used when there's a need for CPU-bound tasks that can't be efficiently handled asynchronously. The choice depends on the nature of the task and the performance requirements of the application.
tokio::task::yield_now().await;
GitHub examples:
Thanks for reading till end , please comment if you have anything .