Author: Eric Sharry

As an important feature of the 2018 Edition, Rust’s asynchronous programming is now widely used. This article attempts to explore the aspects of generator and variable capture, 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.


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

async fn asynchronous() {
    // snipped
}

async fn foo() {
    let x: usize = 233;
    asynchronous().await;
    println!("{}", x);
}
Copy the code

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 in the form of futures 0.12, and local variables intended for use in subsequent asynchronous procedures (such as and_THEN ()) needed to be explicitly and manually chained in and out of closures, which was not a particularly good experience.

All async/.await does is transform the code into a generator/coroutine3 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. The pin4 mechanism needs to be introduced here to solve the possible self-reference problem, which will not be covered in this section.

visualize generator via MIR

We can look through MIR5 to see what the aforementioned generator looks like. MIR is an intermediate representation of RUST, based on the control flow diagram CFG6 representation. 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

That’s all I know.

fn future_1() -> 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 (); // 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; Rs: 27:23:27:23 discriminant((*(_1.0: &mut [static generator@src/anchored. Rs :27:21: Did]))) = 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() {}
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

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() {
    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 Generator trait7, and that “as variant#3” is an operation in MIR, Projection::Downcast is generated by Projection’s own Projection. 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.

anchored

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

Let me first mention one particular thing in rust type systems, auto Trait9. 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.

Anchored10 Crate, for example, provides a gadget implemented via the Auto trait and Generator capture mechanism that prevents variables specified in asynchronous functions 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. Like this:

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

This causes a compilation error:

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

For common types such as STD Mutex, Ref, and RefMut, Clippy provides two LINTS11, which are also implemented by analyzing the type of generator. And the same shortcoming as anchored is that false positives are always present except for placing variables explicitly with a separate block like the one above 12. Because local variables are recorded in other forms 13, information is contaminated.

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… 👋 (Docs. Rs /anchored/0….)

Ref

Appendix

fn future_1: : {closure# 0}(_1: std::pin::Pin<&mut [static generator@src/anchored.rs:35:21: 35:23]>, _2: std::future::ResumeTy) -> std::ops::GeneratorState<(), ()>
let mut _3: ();
let mut _4: std::future::ResumeTy;
let mut _5: u32;
debug _task_context => _4;
fn complex: : {closure# 0}(_1: std::pin::Pin<&mut [static generator@src/anchored.rs:27:20: 33:2]>, _2: std::future::ResumeTy) -> std::ops::GeneratorState<(), ()>
let mut _3: impl std::future::Future;
let mut _4: std::task::Poll<()>;
let mut _5: std::pin::Pin<&mut impl std::future::Future>;
let mut _6: &mut impl std::future::Future;
let mut _7: &mut impl std::future::Future;
let mut _8: &mut std::task::Context;
let mut _9: &mut std::task::Context;
let mut _10: std::future::ResumeTy;
let mut _11: isize;
let _12: ();
let mut _13: std::future::ResumeTy;
let mut _14: ();
let mut _15: impl std::future::Future;
let mut _16: std::task::Poll<()>;
let mut _17: std::pin::Pin<&mut impl std::future::Future>;
let mut _18: &mut impl std::future::Future;
let mut _19: &mut impl std::future::Future;
let mut _20: &mut std::task::Context;
let mut _21: &mut std::task::Context;
let mut _22: std::future::ResumeTy;
let mut _23: isize;
let _24: ();
let mut _25: std::future::ResumeTy;
let mut _26: ();
let _27: ();
let mut _28: std::fmt::Arguments;
let mut_29: & [&str];
let mut_30: & [&str; 3];
let_31: & [&str; 3];
let mut _32: &[std::fmt::ArgumentV1];
let mut _33: &[std::fmt::ArgumentV1; 2];
let _34: &[std::fmt::ArgumentV1; 2];
let _35: [std::fmt::ArgumentV1; 2];
let mut _36: (&i32, &i32);
let mut _37: &i32;
let mut _38: &i32;
let _39: &i32;
let _40: &i32;
let mut _41: std::fmt::ArgumentV1;
let mut _42: &i32;
let mut _43: for<'r.'s.'t0> fn(&'r i32, &'s mut std::fmt::Formatter<'t0>) -> std::result::Result<(), std::fmt::Error>;
let mut _44: std::fmt::ArgumentV1;
let mut _45: &i32;
let mut _46: for<'r.'s.'t0> fn(&'r i32, &'s mut std::fmt::Formatter<'t0>) -> std::result::Result<(), std::fmt::Error>;
let mut_47: & [&str; 3];
let mut _48: ();
let mut _49: std::future::ResumeTy;
let mut _50: u32;
Copy the code