Bridge b/w Sync & Async in Rust
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 Rust, all programs start synchronously. The main function, typically named main, is synchronous by default. You can't directly execute asynchronous code outside of an asynchronous runtime. Asynchronous code relies on an event loop and non-blocking I/O, and it needs to be scheduled by a runtime that can handle tasks asynchronously.
In Rust, when dealing with asynchronous and synchronous code, We need to bridge the gap between the two. This is because Rust's type system enforces a clear separation between synchronous (blocking) and asynchronous (non-blocking) code, and we often need to convert between them when integrating different parts of your application. Here, let me explain why this is required and how it can be achieved.
Rust's approach to asynchronous programming is centered around using asynchronous runtimes to manage and execute asynchronous code. The block_on function is a common entry point for running asynchronous functions. This design allows for flexibility in building applications that are a mix of synchronous and asynchronous code while ensuring that the asynchronous portions of the code are executed efficiently by the runtime.
I think async tasks in Rust are similar to co-routines in Kotlin(suspend and resume instead of blocking) .
When an asynchronous operation is encountered, the runtime suspends the task and schedules other tasks to run in the meantime, then later resumes the suspended task when the operation is completed.
Why Bridging is Required
Bridging between synchronous (sync) and asynchronous (async) code in Rust is necessary due to the fundamental differences in how these two paradigms operate. Understanding why bridging is required is crucial to building efficient, maintainable, and safe applications that combine both sync and async components.
Below are the key reasons why bridging is necessary:
1. Concurrency Models (Synchronous vs. Asynchronous Code):
This is where bridging comes into play. we need mechanisms to convert between asynchronous and synchronous code to ensure that everything works together seamlessly.
Below are some common scenarios where bridging is required or Bridging Techniques :
1. Calling Synchronous Code from an Async Context: If you have an async function and need to use a synchronous function or library (e.g., a blocking database driver), you need to bridge the gap. You can use techniques like block_in_place() to run synchronous code within an async context.Use the block_in_place() is used when you need to run synchronous (blocking) code from within an asynchronous context. It allows you to run a synchronous function within an asynchronous context without blocking the entire runtime.
#cargo.toml
[package]
name = "futures_block_on"
version = "0.1.0"
edition = "2021"
[dependencies]
tokio = { version = "1.29.1", features = ["full"] }
//main.rs
use tokio::main;
fn synchronous_function() {
println!("Inside the Synchronous_function");
}
#[tokio::main]
async fn main() { // This is sync main.
let sync_result = tokio::task::block_in_place(||
synchronous_function());
}
2. Calling Async Code from a Synchronous Context using Futures crate : If your main entry point is a synchronous function (e.g., fn main()), but you need to use asynchronous functionality (e.g., making asynchronous HTTP requests), you need to bridge by running an asynchronous runtime and waiting for the async operations to complete using block_on.The block_on function is provided by the tokio or async-std crate and is used to run an asynchronous future on the current thread. It effectively blocks the current thread until the future completes. You can use block_on to bridge between asynchronous and synchronous code.
#cargo.toml
[package]
name = "block_on"
version = "0.1.0"
edition = "2021"
[dependencies]
futures = "0.3.28"
//main.rs
use futures::executor::block_on;
async fn do_async_work() {
println!("Hello, async world!");
}
fn main() { // sync main
println!("Hello, sync world");
block_on(do_async_work()); // Async call
}
Please note that we are using the futures crate in above example .
Futures Crate :
The futures crate contains traits and functions useful for writing async code. This includes the Stream, Sink, AsyncRead, and AsyncWrite traits, and utilities such as combinators. These utilities and traits may eventually become part of the standard library.
futures has its own executor, but not its own reactor, so it does not support execution of async I/O or timer futures. For this reason, it's not considered a full runtime. A common choice is to use utilities from futures with an executor from another crate.futures - Rust (docs.rs)
领英推荐
Calling async function from Sync code Using tokio lib
use tokio::time::Duration;
use tokio::runtime::Runtime;
async fn async_work() {
// Asynchronous work goes here
println!("Async work in Progress ");
tokio::time::sleep(Duration::from_secs(2)).await;
}
fn main() {
println!("Hello, Amit Nadiger");
// Create a Tokio runtime
let rt = Runtime::new().unwrap();
// Use block_on to call the async function
rt.block_on(async {
async_work().await;
});
}
/*
Hello, Amit Nadiger
Async work in Progress
*/
3. Sharing Data between Sync and Async Code: When you need to share data between synchronous and asynchronous components, you should use synchronization primitives like mutexes (e.g., tokio::sync::Mutex) to ensure safe access to shared resources. Please see the example below.
To protect shared data between asynchronous and synchronous code, you can use a mutex. Libraries like Tokio provide mutex types that can be used for synchronization between asynchronous and synchronous code.
#cargo.toml
[package]
name = "futures_block_on"
version = "0.1.0"
edition = "2021"
[dependencies]
tokio = { version = "1.29.1", features = ["full"] }
use tokio::sync::Mutex;
use std::sync::Arc;
use std::thread;
#[tokio::main]
async fn main() {
// Create a shared DS using an Arc .
let shared_data = Arc::new(Mutex::new(0));
// Clone Arc for use in multiple asynchronous tasks.
let data_clone1 = Arc::clone(&shared_data);
let data_clone2 = Arc::clone(&shared_data);
// Spawn async task that modifies the shared data.
let async_task = tokio::spawn (async move {
let mut data = data_clone1.lock().await;
*data += 101;
// Simulate some asynchronous work here...
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
println!("Async Task: Shared Data: {}", *data);
});
// Spawn sync task that also modifies the shared data.
let sync_task = tokio::task::spawn_blocking(move || {
let mut data = data_clone2.blocking_lock();
// <-- notice "blocking_lock()" API
*data += 100;
// Simulate some synchronous work here...
std::thread::sleep(std::time::Duration::from_secs(1));
println!("Sync task: Shared Data : {}", *data);
});
// Wait for the asynchronous task to complete.
async_task.await.unwrap();
// Wait for the synchronous thread to complete.
sync_task.await.unwrap();
// After both tasks r done, SharedData accessed safely.
let shared_data = shared_data.lock().await;
println!("MMain func: SharedData : {}", *shared_data);
}
/*
O/P
Running `/home/amit/OmPracticeRust/AsyncExperements/Ardan-1HourAsync/target/debug/tokio_block_on`
Async Task: Shared Data after modification: 101
Synchronous Task: Shared Data after modification: 201
Main func : Shared Data : 201
*/
By using mutexes, we can safely share data between asynchronous and synchronous contexts without encountering data races.
4. Running Concurrent Sync and Async Operations: If you want to run synchronous and asynchronous tasks concurrently within the same application, you may need to spawn synchronous tasks as threads or execute async tasks within async runtimes. Please see the example below .
If you have synchronous code that you want to run concurrently, you can spawn it as a separate task using Tokio's tokio::spawn. This doesn't directly bridge between the two, but it allows you to run synchronous code concurrently with asynchronous code.
#cargo.toml
[package]
name = "futures_block_on"
version = "0.1.0"
edition = "2021"
[dependencies]
tokio = { version = "1.29.1", features = ["full"] }
use tokio::main;
fn synchronous_function() {
println!("Inside the Synchronous_function");
}
async fn async_function() {
println!("Inside the Asynchronous_function");
}
#[tokio::main]
async fn main() {
let async_task = tokio::spawn(async_function());
let sync_task = tokio::spawn(async {
synchronous_function();
});
}
/*
Finished dev [unoptimized + debuginfo] target(s) in 4.11s
Running `/home/amit/OmPracticeRust/AsyncExperements/Ardan-1HourAsync/target/debug/tokio_block_on`
Inside the Asynchronous_function
Inside the Synchronous_function
*/
Difference between block_in_place() and block_on() provided in tokio lib:
block_on and block_in_place are two functions provided by asynchronous runtime libraries in Rust, like Tokio, for running asynchronous code from synchronous contexts.
While they serve a similar purpose, there are key differences in their use cases and behavior:
block_on is used for running asynchronous code from an asynchronous context and blocks the current thread until the future completes. On the other hand, block_in_place is used for running synchronous code from an asynchronous context without blocking the entire runtime, allowing other asynchronous tasks to continue executing. The choice between the two functions depends on whether you need to run asynchronous or synchronous code and the impact you want on the concurrency of your application.
block_on:
Use Case: block_on is primarily used to run asynchronous code from within an asynchronous context. It is commonly used when your main function is already asynchronous (e.g., async fn main()) and you want to run asynchronous code inside it.
Behavior: block_on runs the provided future on the current thread's asynchronous runtime, effectively blocking the current thread until the future is completed. It is a way to wait for the result of an asynchronous computation while the rest of the runtime continues to execute other tasks concurrently.
block_in_place:
Use Case: block_in_place is used when you need to run synchronous (blocking) code from within an asynchronous context. It allows you to run a synchronous function within an asynchronous context without blocking the entire runtime.
Behavior: block_in_place temporarily suspends the current asynchronous task and runs the provided synchronous code on the same thread. It does not block the entire runtime, so other asynchronous tasks can continue executing. Once the synchronous code is complete, the asynchronous task is resumed.
Thanks for reading till end , please comment if you know any other methods to bridge between sync and async world in Rust .
Advisor, doer, and experienced board member. Making manufacturers more profitable and sustainable.
11 个月Thanik you for writing this. I wish there was a simpler way to combine synchronous and async code in Rust. The two models are separate, so using something like mpsc to send data between threads becomes difficult. I have a hard time understanding what the performance impacts are, and how to work with them and around them. If I have a choice, is it better to call sync from async, or async from sync? I have a highly data-intensive application where every microsecond counts.
VP, AEM Practice at Bounteous
1 年I typically use `spawn_blocking` from an async context to execute blocking (synchronous) code. `block_in_place` actually calls `spawn_blocking` within its implementation, so if you can use `spawn_blocking` directly, it's a better option with less overhead.