Announcing Tokio 0.3 and the path to 1.0

October 15, 2020

The Tokio team is excited to announce the release of Tokio 0.3. This release functions as a Tokio 1.0 beta. API rough edges have been fixed. This release is an opportunity to validate the changes before stabilizing them as part of the 1.0 release. As most of these issues were small, we expect that upgrading from 0.2 to 0.3 will be easy.

Plan for 1.0

The Tokio team plans to release Tokio 1.0 by the end of December 2020. This date is fast approaching. We ask that you, the Tokio community, try out Tokio 0.3 and give us feedback through GitHub Issues or our Discord channel. Once we release Tokio 1.0, we will commit to the following stability guarantees:

  1. A minimum of 5 years of maintenance.
  2. A minimum of 3 years before a hypothetical 2.0 release.

What’s new?

The main changes in Tokio 0.3 are:

  1. Changes to IO traits.
  2. New runtime builder.
  3. The I/O driver is overhauled
  4. The API is future proofed.

You can find the full list on the changelog.

Changes to IO traits

Following the lead of @sfackler’s RFC, we changed the AsyncRead and AsyncWrite traits to support reading into uninitialized memory. This change only affects users who implement the AsyncRead or AsyncWrite traits or manually calling the poll_* methods. If the AsyncReadExt or AsyncWriteExt traits are used, no change is required.

Below is a simplified version of the new AsyncRead:

pub trait AsyncRead {
    fn poll_read(
        self: Pin<&mut Self>, 
        cx: &mut Context<'_>, 
        buf: &mut ReadBuf<'_>
    ) -> Poll<Result<()>>;
}

pub struct ReadBuf<'a> {
    buf: &'a mut [MaybeUninit<u8>],
    filled: usize,
    initialized: usize,
}

impl<'a> ReadBuf<'a> {
    // functions here, see RFC
}

The &mut [u8] argument to Tokio 0.2’s poll_read method has some unintended sharp edges: an implementer, not the end consumer, of the AsyncRead trait can read the data stored in the &mut [u8] slice. If a programmer creates a mutable slice of bytes (a &mut [u8]) that references uninitialized memory (e.g., the excess capacity in a vector), then reading the &mut [u8] data would cause undefined behavior. The new design mitigates this issue by providing a ReadBuf struct that tracks the uninitialized memory, removing the need to initialize memory.

Additionally, the poll_read_buf and poll_write_buf methods have been removed from both traits. In practice, these methods were rarely implemented. Vectored operations will be implemented directly on types that support them.

Feature flag simplification

We have reduced the amount of feature flags needed to use the core features of Tokio. The dns, tcp, udp, and uds feature flags are collapsed into a single net feature flag. We have also combined rt-core and rt-util into a single rt feature flag. This rt feature flag now encompasses everything needed to execute futures except for the multi-threaded runtime, which now lives under a rt-multi-thread feature flag (was called rt-threaded in 0.2).

  • dns, tcp, udp and uds -> net
  • rt-util and rt-core -> rt
  • rt-threaded -> rt-multi-thread

Runtime and Builder refactor

The Tokio team has improved the runtime Builder to provide better consistency accross feature flag permutations and to be more mis-use resistant. To achieve this, we have added two new constructors to the Builder type to support building both variations of the Tokio runtime. Additionally, Tokio will no longer choose the runtime variant based on feature flags when using Runtime::new, but will instead always use the multi-threaded runtime, which is only available under the rt-multi-thread feature flag. In addition, we’ve renamed the core_threads builder method to the more accurate worker_threads.

We have also refactored the runtime module. Tokio 0.2 exposed two types to interact with the Tokio runtime: Runtime and Handle. Runtime has a block_on method that required &mut self, whereas Handle has a block_on method that required &self. With Tokio 0.3, we collapsed Runtime and Handle into a single Runtime. The block_on method on the new runtime takes &self, which means that it can be put in an Arc without problems.

An example configuring a multi-threaded runtime with 6 worker threads:

use tokio::runtime::Builder;

let rt = Builder::new_multi_thread()
    .worker_threads(6)
    .enable_all()
    .build()
    .unwrap();

rt.block_on(async {
    println!("Hello world!");
});

Methods changed to &self

We have overhauled how we handle wakers within our socket types to allow concurrent calls via &self rather than &mut self. To achieve this, we had to rewrite how we handle wakers internally. The problem comes with how poll_* based methods work. Under the futures contract it states that a poll_* fn should only store the last waker that it was called with.

Note that on multiple calls to poll, only the Waker from the Context passed to the most recent call should be scheduled to receive a wakeup.

From Future

This means that these poll_* methods cannot be called or woken concurrently. To make &self work for our async fn on our sockets types, we refactored our io driver to use an internal intrusive linked-list to store the wakers. By doing this, our socket types can accept &self rather than &mut self when called.

Removal of non-1.0 crates and future proofing for 1.0

In our push for 1.0 we have removed many non-1.0 dependencies from the public API. This includes bytes and mio. This will allow us to push for a future proof and more stable 1.0 while we continue to innovate in our low-level crates.

Mio 0.7

We have finally also upgraded mio to 0.7, which was originally released in an alpha in December of 2019 and did not make the cut for Tokio 0.2. Since then mio 0.7 has had time to mature and is finally making it into this release of Tokio 0.3. This upgrades some key dependencies like winapi from 0.2 to 0.3. Additionally, mio 0.7 now comes with a pure-rust implementation of wepoll which is a library that implements the epoll API for Windows based applications.

Compatibility between 0.2 and 0.3

It is possible to upgrade to Tokio 0.3 incrementally via the tokio-compat-02 crate. This crate will spin up a single threaded background runtime from Tokio 0.2 and will allow you to wrap any future in its context. This will then allow you to run libraries that require Tokio 0.2 inside any runtime, including Tokio 0.3. Below is an example of how you can send a request using hyper 0.13, which requires Tokio 0.2.

use hyper::{Client, Uri};
use tokio_compat_02::FutureExt;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = Client::new();

    // This will not panic because we are wrapping it in the
    // Tokio 0.2 context via the `FutureExt::compat` fn.
    client
        .get(Uri::from_static("http://tokio.rs"))
        .compat()
        .await?;

    Ok(())
}

Conclusion

We've had over 50 contributors help us out since the release of Tokio 0.2. We can't thank our community enough for helping us find bugs and helping us fix them. As we continue to move towards 1.0 feedback will be more critical than ever, so please feel free to open issues or join us on Discord to help us get there.