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.
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
- Rust
- C
- C++
- Java
- C#
#[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
}
dnp3_runtime_t *runtime = NULL;
dnp3_runtime_config_t runtime_config = dnp3_runtime_config_init();
runtime_config.num_core_threads = 4;
dnp3_param_error_t err = dnp3_runtime_create(runtime_config, &runtime);
auto runtime = dnp3::Runtime(dnp3::RuntimeConfig());
private static RuntimeConfig getRuntimeConfig() {
return new RuntimeConfig().withNumCoreThreads(ushort(4));
}
Runtime runtime = new Runtime(getRuntimeConfig());
var runtime = new Runtime(new RuntimeConfig { NumCoreThreads = 4 });
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.
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.
- Rust
- C
- C++
- Java
- C#
Runtime shutdown is implicit in Rust when tokio::main returns.
dnp3_runtime_destroy(runtime);
Runtime shutdown is implicit when the runtime object gets destroyed.
runtime.shutdown();
If you don't call Shutdown
before exiting, the JVM will shutdown the runtime in its finalize method. However, garbage collection is not deterministic, so your program may never properly exit. Make sure you explicitly shut down the runtime before exiting.
runtime.Shutdown();
If you don't call Shutdown
before exiting, the .NET runtime will shut down the runtime in its finalizer (also called the destructor). Again, you should explicitly shut down the runtime before exiting instead of relying on this behavior.