Introduction to the

Coroutines are a very mature concept, and many programming languages (JS, PYTHON, DART, etc.) already provide native support and provide almost the same keyword async/await. Other programming languages that do not provide native support, such as C/C++, have similar library implementations (although the support is not perfect). As a relatively modern programming language, Rust supports asynchronous programming, such as coroutines, in the context of a variety of similar programming languages. Coroutines were introduced to write asynchronous code synchronously, so async/.await is surprisingly easy to use as with other languages. Such as:

use futures::executor::block_on;

async fn hello() {
    let content = async_read("a.txt").await;
    println!("{}", content);
    let content = async_read("b.txt").await;
    println!("{}", content);
}

fn main() {
    let future = hello(); 
    block_on(future);
}
Copy the code

However, Rust only provides the concept of a minimal set of asynchronous objects, and the reputation rust relies on the community, so Runtime leaves it to the community to implement. So now that Rust is using asynchrony on top of rust, the community runtime must be introduced. There are three relatively popular runtime types: Tokio, Async-STD and Smol, with Tokio being the most popular. The advantages and disadvantages of each Runtime and how to use it are not described too much, but refer to related library documents.

While rust asynchronous code is relatively simple to use, it is more complex in principle than in other languages, where runtime is provided at the language level.

The principle of

For single core CPU, there can be only one process at the same time to get right to the use of the CPU, to make the other processes can “and” implementation, the use of the operating system is the CPU time into time slices and to supply other programs to use, so that the operating system can according to the policy control which application can use the CPU. A process exits CPU usage and lets another process use it. This is called a context switch. We often say that context switching is very expensive, because context switching needs to save and reload the program’s running state, call stack, CPU registers and other information, which is very important for high concurrency applications. This way of multitasking is called preemptive multitasking. Coroutines are implemented in a different way called cooperative multitasking, leaving the expensive task of context switching to the application. This avoids the performance penalty caused by frequent context switches. More importantly, write asynchronous code in a synchronous manner. : ~

Rust provides the features of a Future, and the async block transforms the code in the block into a state machine that implements the features of the Future, in much the same way that JS returns promises. The code for the Futrue feature is as follows:

pub trait Future {
    type Output;
    pub fn poll(self: Pin<&mut Self>, ctx: &mut Context<'_>) -> Poll<Self::Output>;
}

pub enum Poll<T> {
    Ready(T),
    Pending,
}
Copy the code

The second parameter in poll, CTX, contains an important content, Waker. If the Runtime polls all the Futrue, poll the next Futrue when poll returns Pending, so that the CPU is always busy or idling, so the Runtime creates the Waker, and the waker tells the Runtime, The Futrue may already be Ready(T) for runtime to poll the Futrue, which will not run out of CPU space.

Using the above example, the compiler might convert code as follows:

enum HelloStateMachine {
    Start(StartState),
    WaitingOnATxt(WaitingOnATxtState),
    WaitingOnBTxt(WaitingOnATxtState),
    End(EndState),
}

impl Future for HelloStateMachine {
    type Output= ();fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
        loop {
            match self{ExampleStateMachine: : Start (state) = > {... } ExampleStateMachine::WaitingATxt(state) => {match state.foo_txt_future.poll(cx) {
                      Poll::Pending => return Poll::Pending,
                      Poll::Ready(content) => {
                          *self = ExampleStateMachine::WaitingBTxt(state);
                        	returnPoll::Ready(content); }}} ExampleStateMachine: : WaitingBTxt (state) = > {... } ExampleStateMachine: : End (state) = > {... } } } } }Copy the code

Of course, the code generated by the compiler is actually more complex, but it is essentially code that produces different state machines, and the above code does not involve state saving. State saving involves a confused characteristic Pin.

Pin is designed to solve the problem of self-referential constructs, since state definitions are basically self-referential constructs, such as states:

struct WaitingState {
    array: ["hello"."world"],
    element: 0x1001cdd.// Element is the address of the last element in arry
}
Copy the code

Element’s address is not updated when WaitingState is moved to a new memory location, invalidating element’s pointer. The simplest way to do this is to allocate elment in the heap and record the memory address on the stack so that no matter how it is moved, the element points to a valid memory address. But unruly students like MEm ::replace or mem::swap can also change the memory address. So the Pin came out. Pin ensures that Poll::Pending always points to the correct address when the state is saved.

summary

Although it is not necessary to be familiar with the principle of async/.await in use, it is of great help to have a deep understanding of the internal principle of async/.await in writing asynchronous code. Since I am not very familiar with Rust, the above is a cursory discussion and may not be completely correct.