A simple, single-threaded async runtime for Rust.
A Runtime runs async tasks, timers and I/O on a single thread. Runtime::block_on runs a
future to completion on the calling thread. While it waits for the future, the same thread also
runs the runtime's tasks, timers and I/O. A runtime comes in two flavours:
LocalRuntimeis the default. It stays on the thread that created it, and runs any'staticfuture,Sendor not. Prefer it unless the runtime, or a future built on it, has to cross threads or be polled by another executor.SharedRuntimecan be used from, and run on, any thread. It runsSendfutures only.
The flavour is a type parameter: LocalRuntime is Runtime<Local>, and SharedRuntime is
Runtime<Shared>. A runtime is cheap to clone, and all its clones refer to the same runtime.
Overview
The runtime, and what runs on it:
- Runtimes:
LocalRuntimeandSharedRuntime, each run byRuntime::block_on. With thehelperfeature, the freeblock_onruns a future on the calling thread's own runtime, whichSharedRuntime::currentreturns. - Tasks:
Runtime::spawn, or the freespawn_localandspawnfor code that has no runtime handle. Each returns aTask. - Timers:
Runtime::sleep,Runtime::timeoutandRuntime::interval. Each has a variant that takes a point in time:Runtime::sleep_until,Runtime::timeout_atandRuntime::interval_at. - Sockets:
TcpListener,TcpStreamandUdpSocketin thenetmodule, and the unix-domain sockets in itsunixmodule, on unix only. - Other I/O sources:
AsyncIo, the async handle of a file descriptor or socket. - Child processes: the
processmodule.
What needs no runtime, and works under any executor:
- Blocking work and files:
unblock(),Unblockand thefsmodule. - Events, locks and channels:
Event, thelockmodule,mpmcandbroadcast.
The runtime and Event are on by default. The rest is behind cargo features.
Examples
A local runtime, used by the thread that created it:
use ;
use LocalRuntime;
let runtime = new.expect;
let doubled = runtime.block_on;
assert_eq!;
A shared runtime, spawned on from another thread and run by the main thread:
use ;
use SharedRuntime;
let runtime = new.expect;
let = channel;
let spawner = spawn;
let task = receiver.recv.expect;
let doubled = runtime.block_on.expect;
spawner.join.expect;
assert_eq!;
Create a runtime with LocalRuntime::new() or SharedRuntime::new(). The compiler cannot infer
the flavour of a plain Runtime::new().
Tasks
Runtime::spawn starts a task on the runtime. It takes a name, which the log message uses if
the task panics.
Code that has no runtime handle can use the free spawn_local and spawn functions instead.
They spawn on the runtime that is running the calling code, and name the task after the place it
was spawned from.
use ;
// Has no runtime handle: it spawns on the runtime that runs its caller.
let runtime = new.expect;
let doubled = runtime.block_on;
assert_eq!;
spawn_localtakes any'staticfuture. Call it from code running on aLocalRuntime: from the future passed to itsblock_on, or from one of its tasks.spawntakes aSendfuture, and spawns it on theSharedRuntimethat runs the calling code. If there is none, it panics, unless thehelperfeature is on (see Per-thread runtimes).
Both return a Task, a handle to the task:
- Awaiting it returns the task's output, or an error if the task panicked or its runtime was dropped.
- Dropping it cancels the task.
Task::cancelcancels the task, and returns a future that completes once it has stopped: with the output, if the task had already finished.Task::detachlets the task run on without a handle.Task::is_finishedtells whether the task has ended, without taking its output.
Timers
Runtime::sleepwaits for a duration, andRuntime::sleep_untilwaits until a deadline.Sleep::resetmoves the deadline of a sleep, for example to restart an idle timeout.Runtime::timeoutputs a time limit on any future, andRuntime::timeout_ata deadline.Runtime::intervalticks once every period, andRuntime::interval_atfrom a start time. TheIntervalthey return is also aStream.
use ;
use LocalRuntime;
let runtime = new.expect;
runtime.block_on;
Sockets
The net module has TCP and UDP sockets: TcpListener, TcpStream and UdpSocket. On unix,
its unix module has UnixListener, UnixStream and UnixDatagram. They are like the sockets of
smol::net, and work on both flavours of runtime. Streams implement AsyncRead and AsyncWrite
from futures-io. Connecting never blocks the thread.
The TCP and UDP sockets take socket addresses, not host names. To look up a name without blocking
the thread, run the lookup with unblock().
Other I/O sources
AsyncIo makes any source the runtime can watch async, like smol::Async. On unix, that is
anything with a file descriptor: a pipe, a terminal, an eventfd, an inotify instance, or a socket
type that net does not cover. On Windows, it can only be a socket. AsyncIo::new takes the
runtime and the source, and switches the source to non-blocking mode.
AsyncIo::read_withandAsyncIo::write_withrun an operation on the source until it no longer fails withWouldBlock.AsyncIo::readableandAsyncIo::writableonly wait for readiness. They are useful when another library does the I/O on the descriptor.AsyncIo<T>implementsAsyncReadfromfutures-ioif&TimplementsRead, andAsyncWriteif&TimplementsWrite.
Any number of tasks can wait through the four methods at once, but only one task per direction
through the AsyncRead and AsyncWrite impls.
AsyncIo is not for regular files: reading or writing one can block the thread, whatever its
readiness says. Use Unblock or the fs module for files.
For code that does its own polling, Runtime::register watches a source and returns a
Registration to poll it through.
Blocking work and files
unblock() runs blocking work on a pool of threads, and returns a future of its result.
Unblock wraps a blocking I/O handle, such as a file or the standard input. It implements the
async I/O traits by running each operation on that pool.
The fs module is async filesystem access, in the shape of std::fs and like smol::fs. It
runs each operation on the same pool.
None of these needs a runtime: they work under any executor.
Child processes
The process module runs child processes, like smol::process. Its Command works like
std::process::Command, but spawns the child on a runtime that you pass to it. The Child it
returns has async status and output methods. The pipes to the child, ChildStdin,
ChildStdout and ChildStderr, implement AsyncWrite and AsyncRead.
Reading or writing a pipe, and waiting for the child to exit, never block the thread. Dropping a
Child leaves the process running, unless kill_on_drop(true) was set on its Command. The
process module documentation has the details.
Events, locks and channels
These work under any executor, not only zruntime's. Tasks on different runtimes and threads can share them.
Eventis a notification that tasks can wait for. A task gets anEventListenerfrom the event and awaits it. Code that changes what the task waits for, for example by releasing a lock, notifies the event. That wakes as many listeners as it asks for.- The
lockmodule has an asyncMutex,RwLockandSemaphore, whose guards can be held across an.await. It also has aBarrier, and aOnceCellwhose initialiser can be async. mpmcis a multi-producer, multi-consumer channel, bounded or unbounded. Each message goes to one receiver.broadcastis a multi-producer, multi-consumer broadcast channel. Each message goes to every receiver.
Running on several threads
One thread at a time runs a runtime, so all its tasks share one CPU core. A task that keeps the
CPU busy holds up every other task on the same runtime. Two threads cannot run one runtime
together: a second block_on on a runtime from SharedRuntime::new panics, and one on a runtime
from SharedRuntime::current waits for its turn.
To use more cores, use several runtimes, one per thread, each run by its own block_on. With the
helper feature, the free block_on already works like this: each thread that calls it runs
its own runtime.
To spread work over the threads, give each thread a clone of the same mpmc receiver. Each
piece of work goes to whichever thread receives it first, as the module's example shows. To run
a task on a specific thread, spawn it on that thread's runtime. A SharedRuntime can be spawned
on from any thread, as the second example above shows.
Per-thread runtimes
The examples above run their runtime with one block_on call for the whole program. A library
such as zbus works differently: it may call block_on once per operation, or have its futures
polled by another executor, and its tasks and timers must keep running in between.
The helper feature supports this:
- The free
block_onruns a future on the calling thread's own runtime. SharedRuntime::currentreturns the runtime for the calling code. From the future passed to the freeblock_on, or from code running on a runtime thatcurrentreturned, it is that runtime. From anywhere else, code on a runtime fromSharedRuntime::newincluded, it is one runtime that the whole process shares.- If such a runtime has work and no thread is running
block_onon it, a helper thread runs it in the meantime. Ablock_oncall takes the runtime back for as long as it lasts. spawnworks from any thread. If no shared runtime runs the calling code, the task goes on the runtime thatSharedRuntime::currentreturns.
Combinators
zruntime has no combinators of its own. Use those of the futures crate instead. They work with
zruntime's tasks, timers, sockets and pipes, which implement the Future, Stream,
AsyncRead and AsyncWrite traits.
futures::futurejoins and races futures.futures::streamadapts streams, such as an interval or the connections of a listener.futures::ioadds methods such asread_to_endandwrite_all, and aBufReaderto read lines with. TheUnblockdocumentation shows it reading the standard input line by line.
The default features of futures bring an executor, which you do not need with zruntime. They
also bring the join! and select! macros, which build a proc-macro crate. To leave both out:
[]
= { = "0.3", = false, = ["std"] }
std brings the I/O extension traits. Add async-await to get the macros back.
use Duration;
use future;
use LocalRuntime;
let runtime = new.expect;
runtime.block_on;
To race a future against a timer, use Runtime::timeout.
Cargo features
runtime(default):Runtime,LocalRuntime,SharedRuntime, tasks, timers,AsyncIoandRegistration.event(default):EventandEventListener.tracing(default): log throughtracing. Without it, the runtime logs nothing.helper: per-thread runtimes. Impliesruntime.lock: thelockmodule. Impliesevent.mpmc: thempmcmodule. Impliesevent.broadcast: thebroadcastmodule. Impliesevent.unblock:unblock()andUnblock.fs: thefsmodule. Impliesunblockandlock.tcp:TcpListenerandTcpStreamin thenetmodule. Impliesruntime.udp:UdpSocketin thenetmodule. Impliesruntime.unix: thenet::unixmodule, on unix only. Impliesruntime.process: theprocessmodule. Impliesruntimeandunblock.
runtime and event do not depend on each other. A crate that only needs Event, or the locks
and channels built on it, can turn off the default features. It then enables only what it uses,
such as features = ["lock"], and builds none of the runtime.
Why?
The project grew out of the need for a single-threaded runtime in zbus that it would use by default. It was split into a separate project so non-zbus users can use it too.