Runtime

In the previous section we explored async fn Futures which allow us to represent a value that will be available “at some point in the future”. We mentioned that a Rust Future requires something to poll it for completion and said that the something is the Tokio runtime.

Tokio runtime

The Runtime is responsible for repeatedly calling poll on a Future until its value is returned. There are a few different ways this can happen in practice. For example, the basic_scheduler configuration will block the current thread and process all spawned tasks in place. The threaded_scheduler configuration uses a work-stealing thread pool and distributes load across multiple threads. The threaded_scheduler is the default for applications and the basic_scheduler is the default for tests.

Ultimately all asynchronous code must be polled to do any work. Polling the Future is the job of the Tokio runtime, but you must tell Tokio about the Future for this to happen. You can directly tell Tokio about the Future with the tokio::spawn function, but you can also use .await inside something Tokio already knows about. In this example, we don’t tell Tokio about the Future created by the asynchronous function TcpStream::connect.

# #![allow(unused_must_use)]
use tokio::net::TcpStream;

#[tokio::main]
async fn main() {
  // Create a tcp stream, but do not call await.
  TcpStream::connect("127.0.0.1:6142");
}

This code will do nothing. Therefore, the compiler produces the warning below, reminding you that the future must be awaited in order for it to be executed.

warning: unused implementer of `std::future::Future` that must be used
 --> src/main.rs:6:3
  |
6 |   TcpStream::connect("127.0.0.1:6142");
  |   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |
  = note: `#[warn(unused_must_use)]` on by default
  = note: futures do nothing unless you `.await` or poll them

Spawning Tasks

One of the unique aspects of Tokio is that futures can be spawned on the runtime from within other async tasks. Tasks are the application’s “unit of logic”. They are similar to Go’s goroutine and Erlang’s process, but asynchronous. In other words, tasks are asynchronous green threads.

Tasks are passed to the runtime, which handles scheduling the task. The runtime is usually scheduling many tasks across a single or small set of threads. Tasks must not perform computation-heavy logic or they will prevent other tasks from executing. So don’t try to compute the fibonacci sequence as a task!

We can spawn tasks using tokio::spawn. For example:

#[tokio::main]
async fn main() {
    let handle = tokio::spawn(async {
        println!("doing some work, asynchronously");

        // Return a value for the example
        "result of the computation"
    });

    // Wait for the spawned task to finish
    let res = handle.await;

    println!("got {:?}", res);
}

Again spawning tasks can happen within other futures or streams allowing multiple things to happen concurrently. In the above example we’re spawning the inner future from within the outer stream. Each time we get a value from the stream we’ll simply run an inner future.

In the next section, we’ll take a look at a more involved example than our hello- world example that takes everything we’ve learned so far into account.

下一篇Network Utilities