24 days from Node.js to Rust

preface

Futures is the core of Rust asyncization. The Rust standard library defines a specification for asynchronous tasks through the Futures trait. However, implementing Futures is not enough to implement asyncization, you also need to manage them. You need to check which futures are done and which are waiting, and you need an Executor and reactor (similar to the Event loop in Node.js). Rust doesn’t provide that, but the Rust team leaves it up to the community to decide how to build a better asynchronous ecosystem. This may seem crazy, but remember that in the early days of JavaScript there were no promises, it was all community contributions

The body of the

Asynchronous library

  • Tokio (repo) (crates.io) (docs.rs)
  • Async-std (repo) (crates.io) (docs.rs)
  • Smol (repo) (crates.io) (docs.rs)

There are many more, but these are enough. Each library has its own audience, and it is unwise to discuss who is better. If one were to consider which would be the most convenient solution, the obvious answer would be Tokio. Some libraries already rely on Tokio, and it would be difficult for an executor who doesn’t use Tokio to use them. But Tokio is actually pretty good, with documentation, plenty of community contributions, and plenty of sample code to learn from

There were a number of server-side JavaScript implementations before Node.js, some of which were even single-threaded, requiring the developer to fork themselves to handle blocking. Tokio is similar to Node.js in that it provides asynchronous methods you can use. Smol, like Deno, promises to be faster and better

start

We added the Tokio dependency to Cargo. Toml and needed to specify the full flag

[dependencies]
tokio = { version = "1", features = ["full"]}Copy the code

Features is used to expose conditional compilation, which is optional and is used by some libraries to enable or disable platform customization code. In Tokio, it is used to import sub-crates on demand. Tokio used to split it into several smaller modules, but the community changed it to include features

You have to start an executor before you can run Futures. Tokio provides a macro that does everything for you behind the scenes. Add it to the main() function and you have a complete asynchronous Rust program

#[tokio::main]
async fn main()  { // Notice we write async main() now
}
Copy the code

async/.await

Rust has an async/await style syntax similar to JavaScript. Adding async to a function changes its return value from T to impl Future, for example:

fn regular_fn() - >String {
  "I'm a regular function".to_owned()
}

async fn async_fn() - >String { // actually impl Future<Output = String>
  "I'm an async function".to_owned()
}
Copy the code

Unlike JavaScript, “await” in Rust must be bound to a specific future and cannot precede or be used with any value other than the future

#[tokio::main]
async fn main() {
    let msg = async_fn().await;
}
Copy the code

Unlike JavaScript, the future is not executed until await is called

#[tokio::main]
async fn main() {
    println!("One");
    let future = prints_two();
    println!("Three");
    // Uncomment the following line of code to see the effect
    // future.await;
}

async fn prints_two() {
    println!("Two")}Copy the code

Results:

One
Three
Copy the code

The result with the comment removed:

One
Three
Two
Copy the code

async block

Asynchrony and closures are essential tools for every developer, and you’re bound to get the following error when returning an asynchronous closure:

error[E0658]: async closures are unstable
 --> src/send-sync.rs:6:15
  |
6 |     let fut = async || {};
  |               ^^^^^
  |
  = note: see issue #62290 <https://github.com/rust-lang/rust/issues/62290> for more information
  = help: to use an async block, remove the `||`: `async {`
Copy the code

Asynchronous closures are not very stable, and the compiler will tell you that you are using async blocks. What is an asynchronous block?

Virtually all blocks can be asynchronized, they implement a Future, and can be returned just like any other data

#[tokio::main]
async fn main() {
    let msg = "Hello world".to_owned();

    let async_block = || async {
        println!("{}", msg);
    };
    async_block().await;
}
Copy the code

You can get the full power of an asynchronous closure by returning an asynchronous block

#[tokio::main]
async fn main() {
    let msg = "Hello world".to_owned();

    let closure = || async {
        println!("{}", msg);
    };
    closure().await;
}
Copy the code

Send + Sync

Mixing threads and futures will cause you to start getting errors related to Send and Sync, usually the Future Cannot be sent between Threads safely. This error is shown in the following code, which will not compile:

use std::fmt::Display;
use tokio::task::JoinHandle;

#[tokio::main]
async fn main() {
    let mark_twain = "Samuel Clemens".to_owned();

    async_print(mark_twain).await;
}

fn async_print<T: Display>(msg: T) -> JoinHandle<()> {
    tokio::task::spawn(async move {
        println!("{}", msg);
    })
}
Copy the code

Results:

error: future cannot be sent between threads safely --> src/send-sync.rs:12:5 | 12 | tokio::task::spawn(async move { | ^^^^^^^^^^^^^^^^^^ future created by async block is not `Send` | note: captured value is not `Send` --> src/send-sync.rs:13:24 | 13 | println! ("{}", msg);
    |                        ^^^ has type `T` which is not `Send`
note: required by a bound in`tokio::spawn` --> Ecc6299db9ec823 / tokio/Users/jsoverson/.cargo/registry/src/github.com - 1-1.15.0 / SRC/task/spawn. Rs: 127:21 127 | | T: Future + Send +'static, | ^^^^ required by this bound in `tokio::spawn` help: consider further restricting this bound | 11 | fn async_print
      
       (msg: T) -> JoinHandle<()> { | +++++++++++++++++++
      Copy the code

Send and Sync are at the heart of Rust’s concurrency implementation. They are automatic traits, meaning that Rust automatically adds Send or Sync to all component types if they are Send or Sync. These traits indicate whether a type can be safely transmitted across multiple threads, without which problems with data across multiple threads would arise

Fortunately, many Rust types are Sync and Send. You just need to be careful how to eliminate errors. You can simply add + Send, + Sync or + Sync + Send to traits

fn async_print<T: Display + Send>(msg: T) -> JoinHandle<()> {
    tokio::task::spawn(async move {
        println!("{}", msg); })}Copy the code

But there are new problems with writing this way:

error[E0310]: the parameter type `T` may not live long enough
   --> src/send-sync.rs:12:5
    |
11  | fn async_print<T: Display + Send>(msg: T) -> JoinHandle<()> {
    |                -- help: consider adding an explicit lifetime bound... : `T:'static +` 12 | tokio::task::spawn(async move { | ^^^^^^^^^^^^^^^^^^ ... so that the type `impl Future` will meet its required lifetime bounds... |Copy the code

We encountered the ‘static ‘problem in Tutorial 16. Since the Rust compiler knows it can’t tell when asynchronous code executes, it will tell us that T may not be around long enough. We need to let the Rust compiler know that this type can be around forever

fn async_print<T: Display + Send + 'static>(msg: T) -> JoinHandle<()> {
    tokio::task::spawn(async move {
        println!("{}", msg); })}Copy the code

There’s more to Sync and Send, which we’ll talk about later

reading

  • Rust docs: Future
  • Rust docs: async
  • Asynchronous Programming in Rust book
  • Rustonomicon: Send & Sync

conclusion

Rust’s asynchronous programming is beautiful enough to write 24 articles. Rust’s memory mechanism allows you to safely write multithreaded, asynchronous code, and this is where you need to start stepping up your efforts to leave JavaScript behind. You can use threads and Web workers in Node.js, but it’s a compromise. In the Rust world, we do not need such compromises