Runtime

Rust's asynchronous programming model is a form of cooperative multitasking. Once a task reaches a point where it would normally block (e.g. reading from a socket), execution is instead released back to an executor so that another task may run. A pool of worker threads inside the executor can 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 could be written by hand. Although Rust has this capability built into the compiler, it does not include a default runtime on which to execute the asynchronous programs. Instead, users are free to pick the runtime as an external library.

The DNP3 library runs on top of the Tokio runtime which provides a state-of-the-art scheduler and platform-agnostic networking APIs. Under the hood, an efficient OS-specific mechanism is used, e.g. epoll on Linux or 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 make asynchronous programs that are not only incredibly fast, but also correct. It is particularly hard to write correct asynchronous software in C/C++ because of the need to manually reason about object lifetimes in callbacks.

Lifetime#

A Runtime must be created before any communication can take place. It is a shared resource for multiple communication sessions. It is typically created just after initializing logging and is the last component to be shut down. Runtime shutdown is covered in the last section of this page.

note

Rust users may even share the runtime with other libraries that also use Tokio. The bindings currently do not allow for runtime sharing, but it may 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

When using any of the bindings, setting the number of runtime threads to 0 will default to the number of system cores. This is a safe default that will lead to good multi-core utilization.

Callbacks#

Callbacks from the library to user code are invoked from the runtime's thread pool. This means that blocking during a callback will make an entire thread unavailable for task execution. If all of the thread-pool threads are blocked, no communication sessions will be able to execute.

Applications should try to avoid blocking whenever possible. For example, when a log message is received via a callback, a synchronous call to write the message to a file will block a thread. If this is occurring frequently on all of the pool threads, it can cause poor throughput or even task starvation. Applications should defer blocking calls to dedicated worker threads, e.g. a user-managed thread that writes log messages to file.

tip

In cases where some blocking is unavoidable, it can be useful to set the number of worker threads to a multiple of the number of system cores, e.g. 2x or 3x the number of system cores.

Shutdown#

Shutting down a Runtime will stop all of the master and outstation communication associated with it. This is typically the last DNP3-related operation your program should perform before exit.

note

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