Skip to main content

Runtime

Rust's asynchronous programming model is a form of cooperative multitasking. Once a task gets to a point where it would typically block, such as reading from a socket, execution is instead released back to an executor so that another task may run. This lets a pool of worker threads inside the executor efficiently execute thousands of asynchronous tasks concurrently without the overhead of per-task call stacks or thread context-switching.

Rust supports asynchronous programming using 'async' functions and 'async/await' syntax. The Rust compiler transforms synchronous-looking code into state machines that are just as efficient as what can be written by hand. Although Rust has this capability built into the compiler, it doesn't include a default runtime to execute the asynchronous programs. Instead, you are free to pick the runtime as an external library.

The DNP3 library runs on top of the Tokio runtime, providing a state-of-the-art scheduler and platform-agnostic networking APIs. The OS-specific mechanisms vary by platform, for example, epoll on Linux and IOCP on Windows.

Tokio is a modern evolution of libraries like libuv (C) and ASIO (C++). It leverages Rust's thread and memory safety to deliver asynchronous programs that are not only incredibly fast, but also correct. This is extremely important since it is quite difficult to write correct asynchronous software in C/C++ due to the need to manually reason object lifetimes in callbacks.

Lifetime

You must create a Runtime before any communication can take place. It is a shared resource for multiple communication sessions that is typically created just after initializing logging. It is also the last component to shut down; see below for more details about runtime shutdown.

note

Rust users can share the runtime with other libraries that also use Tokio. The bindings don't currently support sharing a runtime, but this will be possible in a future release.

Examples

#[tokio::main(flavor = "multi_thread")]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// do things within the context of the runtime
// it is automatically shut down when this async fn returns
}
tip

Set the number of runtime threads to 0 to default to the number of system cores. This provides a safe default that will lead to good multi-core utilization.

Callbacks

The runtime's thread pool invokes callbacks from the library to user code. If you block during a callback, an entire thread is made unavailable for task execution. If all threads in a thread pool are blocked, no communication sessions will execute until a thread becomes unblocked.

For example, when you receive a log message via a callback, a synchronous call to write the message to a file will block a thread. If this frequently occurs on all your pool threads, it can cause poor throughput or even task starvation.

For best results, avoid blocking whenever possible in your applications. Instead, you should defer blocking calls to dedicated worker threads, such as a user-managed thread that write log messages to file.

tip

If you have a case where some blocking is unavoidable, set the number of worker threads to a multiple of the number of system cores, such as 2x or 3x.

Shutdown

When you shut down a Runtime, it stops all tasks associated with it. This is typically the last operation your program should perform before exit.

note

Runtime shutdown is implicit in Rust when tokio::main returns.