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 rodbus 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 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

Shutting down the runtime is typically one of the last operations your program should perform before exiting. The call to shutdown is synchronous. When it completes you are assured there are no longer any tasks running and all of the Tokio worker threads have been joined.

danger

A blocked worker thread can cause shutdown to deadlock. For example, if a communication channel makes a callback to user code that permanently blocks, shutdown will cause a deadlock.

If you cannot ensure a clean shutdown, you can use Runtime.set_shutdown_timeout(..) to put an upper time limit on the eventual shut down. You would call this method immediately after creating the Runtime.

Shutting down the runtime using a timeout can leak memory as worker threads are be aborted if the timeout occurs. Only use this method if you are exiting the process anyway.

Logging

Both the creation and the shutdown of the Runtime are logged in the bindings:

Jan 12 16:16:42.004  INFO creating runtime with 24 threads

.......

Jan 12 16:36:53.832 INFO beginning runtime shutdown (no timeout)
Jan 12 16:36:53.838 INFO runtime shutdown complete
note

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