Article | Ruihang Xia

Currently involved in the Edge timing data storage engine project

Read this article in 6992 words for 18 minutes

Before the speech

An important feature of the 2018 Edition, Rust’s asynchronous programming is now widely used. While it’s hard not to wonder how it works, this article attempts to explore the generator and variable capture aspects, and then introduces a scenario encountered during the development of the embedded timing storage engine CeresDB-Helix.

Due to the author’s level of content, there are some mistakes and omissions, please leave a message to inform.

PART. 1 async/.await, coroutine and generator

The async/.await syntax entered stable Channel in version 1.39 [1] and makes it easy to write asynchronous code:

,,, Java async fn asynchronous() {// snipped}

async fn foo() { let x: usize = 233; asynchronous().await; println! (“{}”, x); ,,,,

In the example above, the local variable X can be used directly after an asynchronous process (FN asynchoronous), just like writing synchronous code. Prior to this, asynchronous code was typically used through combinators of the form futures 0.1[2], and local variables intended for use in subsequent asynchronous procedures (such as and_THEN ()) needed to be explicitly and manually chained as closure input and exit arguments, which was not a particularly good experience.

All async/.await does is transform the code into a generator/coroutine[3] to execute. A coroutine procedure can be suspended to do something else and then resume execution, which is currently used as.await. For example, in the above code, another asynchronous procedure asynchronous() is called in asynchronous procedure foo(), and execution of the current procedure is suspended at.await on line 7 and resumed when it is ready to continue.

Resuming execution may require some previous information, as in foo() we used the previous information x in line 8. That is, an async procedure should be able to hold some internal local state so that it can be used after.await. In other words, store local variables in generator State that may be used after yield. Here, THE MECHANISM of PIN [4] needs to be introduced to solve the possible self-reference problem, which will not be described in this part.

PART. 2 visualize generator via MIR

We can see what the aforementioned generator looks like through MIR[5]. MIR is an intermediate representation of Rust, based on the representation of control flow graph CFG[6]. CFG is a good way to visualize what a program might look like when it executes, and MIR can help when it’s not clear what your Rust code has become.

There are several ways to get a MIR representation of your code. If you now have a Rust Toolchain available, you can pass an environment variable to RUSTC like this and build with Cargo to generate MIR:

RUSTFLAGS="--emit mir" cargo build
Copy the code

A.mir file will be generated in target/debug/deps/ if the build is successful. Or you can retrieve MIR from play.rust-lang.org/ by selecting MIR from the Overflow menu next to Run.

The MIR generated by the 2021-08 NIGHTLY toolchain looks something like this, but there’s a lot of stuff to forget about.

  • _0, _1 these are variables

  • Much of Rust’s syntax is similar, such as type annotations, function definitions and calls, and annotations.

fn future_1(a) -> impl Future {
    let mut _0: impl std::future::Future; // return place in scope 0 at src/anchored.rs:27:21: 27:21
    let mut _1: [static generator@src/anchored.rs:27:21: 27:23]; // in scope 0 at src/anchored.rs:27:21: 27:23

    bb0: {
        discriminant(_1) = 0; // scope 0 at src/anchored.rs:27:21: 27:23
        _0 = from_generator::<[static generator@src/anchored.rs:27:21: 27:23]>(move _1) -> bb1; // scope 0 at src/anchored.rs:27:21: 27:23
                                         // mir::Constant
                                         // + span: src/anchored.rs:27:21: 27:23
                                         // + literal: Const { ty: fn([static generator@src/anchored.rs:27:21: 27:23]) -> impl std::future::Future {std::future::from_generator::<[static generator@src/anchored.rs:27:21: 27:23]>}, val: Value(Scalar(<ZST>)) }
    }

    bb1: {
        return; // scope 0 at src/anchored.rs:27:23: 27:23
    }
}

fn future_1::{closure#0}(_1: Pin<&mut [static generator@src/anchored.rs:27:21: 27:23]>, _2: ResumeTy) -> GeneratorState<(), ()> {
    debug _task_context => _4; // in scope 0 at src/anchored.rs:27:21: 27:23
    let mut _0: std::ops::GeneratorState<(), ()>; // return place in scope 0 at src/anchored.rs:27:21: 27:23
    let mut _3: (); // in scope 0 at src/anchored.rs:27:21: 27:23
    let mut _4: std::future::ResumeTy; // in scope 0 at src/anchored.rs:27:21: 27:23
    let mut _5: u32; // in scope 0 at src/anchored.rs:27:21: 27:23

    bb0: {
        _5 = discriminant((*(_1. 0: &mut [static generator@src/anchored.rs:27:21: 27:23)));// scope 0 at src/anchored.rs:27:21: 27:23
        switchInt(move _5) -> [0_u32: bb1, 1_u32: bb2, otherwise: bb3]; // scope 0 at src/anchored.rs:27:21: 27:23
    }

    bb1: {
        _4 = move _2; // scope 0 at src/anchored.rs:27:21: 27:23
        _3 = const(a);// scope 0 at src/anchored.rs:27:21: 27:23
        ((_0 as Complete). 0: ()) = move _3; // scope 0 at src/anchored.rs:27:23: 27:23
        discriminant(_0) = 1; // scope 0 at src/anchored.rs:27:23: 27:23
        discriminant((*(_1. 0: &mut [static generator@src/anchored.rs:27:21: 27:23=)))1; // scope 0 at src/anchored.rs:27:23: 27:23
        return; // scope 0 at src/anchored.rs:27:23: 27:23
    }

    bb2: {
        assert(const false."`async fn` resumed after completion") -> bb2; // scope 0 at src/anchored.rs:27:21: 27:23
    }

    bb3: {
        unreachable; // scope 0 at src/anchored.rs:27:21: 27:23}}Copy the code

There is some other code in Demo Crate, but the source code for MIR is relatively simple:

async fn future_1(a) {}
Copy the code

Just a simple empty asynchronous function, you can see that the generated MIR will swell up a lot, and if the content is a little bit more, it will not look good in text form. We can specify the format of the generated MIR and then visualize it.

The steps are as follows:

RUSTFLAGS="--emit mir -Z dump-mir=F -Z dump-mir-dataflow -Z unpretty=mir-cfg" cargo build > mir.dot
dot -T svg -o mir.svg mir.dot
Copy the code

You can find mir.svg in the current directory, and when you open it, you’ll see what looks like a flowchart (a similar image has been omitted, if you’re interested, try generating your own).

Here, MIR is organized according to the basic unit basic block (BB), the original information is present, and the jump relationship between each basic block is drawn. In the diagram above we can see that there are four Basic blocks, one of which is the starting point and the other three are the finishing points. The bb0 switch (match in rust) starts with a variable _5, which branches to different blocks according to the value. Imagine code like this:

match _5 {
  0: jump(bb1),
    1: jump(bb2),
    _ => unreachable()
}
Copy the code

The state of the generator can be thought of as _5, and the different values are the states of the generator. The state of future_1 would look something like this

enum Future1State {
    Start,
    Finished,
}
Copy the code

In the case of async fn foo() in §1, there might be an additional enumerated value to indicate that yield. Thinking back to the previous question at this point, it is natural to think about how variables that span different stages of the generator need to be saved.

enum FooState {
    Start,
    Yield(usize),
    Finished,
}
Copy the code

PART. 3 generator captured

Let’s call captured variables stored in generator state that can be used by subsequent phases across.await/yield. So is it possible to know which variables are actually captured? Let’s give it a try and write a slightly more complex asynchronous function first:

async fn complex(a) {
    let x = 0;
    future_1().await;
    let y = 1; future_1().await; println! ("{}, {}", x, y);
}
Copy the code

The generated MIR and SVG are quite complex, so a paragraph was intercepted and put in the appendix. You can try to generate a complete content by yourself.

Glancing at the generated content, we can see that a long type always appears, something like this:

[static generator@src/anchored.rs:27:20: 33:2]
// or(((* (_1. 0: &mut [static generator@src/anchored.rs:27:20: 33:2])) as variant#3). 0: i32)
Copy the code

By looking at the location of our code, we can see that the two file locations in this type are the opening and ending curly braces of our asynchronous function complex(), which is a type associated with our entire asynchronous function.

Upon further exploration we can probably guess that the first line of the code snippet above is an anonymous type (struct) that implements the Generator trait[7], and that “as variant#3” is an operation in MIR, The Projection::Downcast of Projection is roughly generated here [8]. The projection type made after this downcast is known as i32. Combining other similar fragments, we can infer that this anonymous type is similar to the generator state described above, and that each variant is a different state tuple. Projecting this N-tuple can get the captured local variable.

PART. 4 anchored

Knowing which variables are captured can help us understand our code and make some applications based on that information.

First, the Auto trait[9] is a special feature of the Rust type system. The most common are Send and Sync. The auto traits are automatically implemented for all types unless negative impl opt-out is explicitly used and the negative impl is passed, such as including! Send’s Rc structure is the same! The Send. With Auto Traits and Negative impl we control the types of some structures and let the compiler check them for us.

For example the anchored[10] Crate provides a gadget implemented via the Auto trait and Generator capture mechanism that prevents variables specified in an asynchronous function from passing through the.await point. One of the more useful scenarios is fetching variability within a variable during an asynchronous process.

Typically, we use asynchronous locks such as Tokio ::sync::Mutex to provide internal variability; If the variable is captured by generator state without passing through.await point, synchronization locks such as STD ::sync::Mutex or RefCell can also be used; If you want higher performance and avoid the overhead of both runtimes, you can also consider UnsafeCell or another unsafe approach, but it’s a little risky. And with anchored we can control the insecurity in this scenario and implement a secure way to provide the internal variability by marking the variable anchored with the ZST anchored:: anchored, Attaching an attribute to the entire Async FN will allow the compiler to make sure that nothing is caught incorrectly and passed through.await, leading to disastrous data contention.

Something like this:

#[unanchored]
async fn foo(a){
    {
        let bar = Anchored::new(Bar {});
    }
    async_fn().await;
}
Copy the code

This causes a compilation error:

#[unanchored]
async fn foo(a){
    let bar = Anchored::new(Bar {});
    async_fn().await;
    drop(bar);
}
Copy the code

For common types of STD such as Mutex, Ref, and RefMut, Clippy provides two LINTs [11], which are also implemented by analyzing the type of generator. And like the anchored, both have a shortcoming of false positive except for placing variables explicitly with a separate block like the one above [12]. Because local variables will be recorded in other forms [13], information will be polluted.

The Anchored currently lacks some ergonomic interfaces, and the Attribute Macro has a bit of a problem interacting with other tools of ecosystem. If you’re interested, check out github.com/waynexia/an…

Documents: the docs. The rs/anchored / 0….

“Barometer”

[1] blog.rust-lang.org/2019/11/07/…

[2] docs. Rs/futures / 0.1…

[3] github.com/rust-lang/r…

[4] doc.rust-lang.org/std/pin/ind…

[5] blog.rust-lang.org/2016/04/19/…

[6] en.wikipedia.org/wiki/Contro…

[7] doc.rust-lang.org/std/ops/tra…

[8] github.com/rust-lang/r…

[9] doc.rust-lang.org/beta/unstab…

[10] crates. IO/crates/anch…

[11] rust – lang. Making. IO/rust – clippy…

[12] github.com/rust-lang/r…

[13] doc.rust-lang.org/stable/nigh…

Recommended Reading of the Week

Prometheus on CeresDB Evolution process

Go deep into HTTP/3 (I) | The evolution of the protocol from the establishment and closure of QUIC links

Reduce cost and improve efficiency! The transformation of the registration center in ant Group

Practice of Etcd resolution of large scale Sigma cluster in ants