Bridge b/w Sync & Async in Rust

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):

  1. Synchronous Code:Synchronous code follows a traditional execution model where each operation blocks the thread until it completes.In Rust, synchronous code is typically written using blocking functions or methods.
  2. Asynchronous Code:Asynchronous code allows operations to run concurrently without blocking the execution of the entire program.In Rust, asynchronous code is written using async/await syntax, allowing non-blocking execution of tasks.
  3. Strict Separation:Rust's type system is designed to prevent mixing synchronous and asynchronous code seamlessly. It enforces a clear separation between the two styles.This separation helps prevent accidental blocking in asynchronous contexts, which can lead to performance issues and defeat the purpose of using asynchronous programming.
  4. Type System:Rust's type system plays a role in enforcing this separation by distinguishing between types and traits that are specific to synchronous or asynchronous programming.For example, Rust has different types for handling synchronous and asynchronous I/O operations (std::io::Result for synchronous I/O and tokio::io::* for asynchronous I/O).
  5. Compatibility and Interoperability:While Rust enforces a strict separation, it also provides mechanisms for interoperability between synchronous and asynchronous code when necessary.Libraries and frameworks in Rust often provide both synchronous and asynchronous APIs, allowing developers to choose the appropriate style for their needs.
  6. Concurrent Execution:Rust's asynchronous model is well-suited for scenarios where multiple tasks can progress concurrently without blocking, making it particularly useful for I/O-bound and network-bound operations.
  7. Resource Management: Synchronous code and asynchronous code might manage resources differently. In synchronous code, we usually rely on traditional blocking I/O or synchronous data structures, while asynchronous code often uses non-blocking I/O and asynchronous primitives. Bridging mechanisms help in managing these resources and converting between them as needed.
  8. Integration and Compatibility In real-world applications, we often need to integrate asynchronous and synchronous components. For example, our main application entry point might be asynchronous (e.g., async fn main()), but we might need to call a synchronous function from a library that doesn't support asynchronous operations or call an asynchronous function from synchronous 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 .

Niels Erik Andersen

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.

Steven Carter

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.

要查看或添加评论,请登录

Amit Nadiger的更多文章

  • Rust modules

    Rust modules

    Referance : Modules - Rust By Example Rust uses a module system to organize and manage code across multiple files and…

  • List of C++ 17 additions

    List of C++ 17 additions

    1. std::variant and std::optional std::variant: A type-safe union that can hold one of several types, useful for…

  • List of C++ 14 additions

    List of C++ 14 additions

    1. Generic lambdas Lambdas can use auto parameters to accept any type.

    6 条评论
  • Passing imp DS(vec,map,set) to function

    Passing imp DS(vec,map,set) to function

    In Rust, we can pass imp data structures such as , , and to functions in different ways, depending on whether you want…

  • Atomics in C++

    Atomics in C++

    The C++11 standard introduced the library, providing a way to perform operations on shared data without explicit…

    1 条评论
  • List of C++ 11 additions

    List of C++ 11 additions

    1. Smart Pointers Types: std::unique_ptr, std::shared_ptr, and std::weak_ptr.

    2 条评论
  • std::lock, std::trylock in C++

    std::lock, std::trylock in C++

    std::lock - cppreference.com Concurrency and synchronization are essential aspects of modern software development.

    3 条评论
  • std::unique_lock,lock_guard, & scoped_lock

    std::unique_lock,lock_guard, & scoped_lock

    C++11 introduced several locking mechanisms to simplify thread synchronization and prevent race conditions. Among them,…

  • Understanding of virtual & final in C++ 11

    Understanding of virtual & final in C++ 11

    C++ provides powerful object-oriented programming features such as polymorphism through virtual functions and control…

  • Importance of Linux kernal in AOSP

    Importance of Linux kernal in AOSP

    The Linux kernel serves as the foundational layer of the Android Open Source Project (AOSP), acting as the bridge…

    1 条评论

社区洞察

其他会员也浏览了