Runtime
The library runs on Tokio, the state-of-the-art asynchronous runtime behind much of Rust's networking software. Its work-stealing scheduler runs thousands of lightweight tasks on a small pool of threads, and its I/O uses each operating system's most efficient mechanism. Every master channel and outstation session is one of those tasks, so a single runtime scales from one session to thousands without a thread per connection.
One Runtime runs any mix of masters and outstations. A protocol gateway, for example, can poll its field devices as a
master and serve its control center as an outstation, all on the same runtime.
Lifetime
Create a Runtime before any communication can take place. It's a shared resource for multiple sessions, typically created just after initializing logging and shut down last when exiting.
Rust users can share the runtime with other libraries that also use Tokio. The bindings don't support sharing a runtime.
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 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.
Avoid blocking in callbacks whenever possible. Defer blocking calls to dedicated worker threads, such as a separate thread that writes log messages to file.
Binding methods that wait for the runtime to complete an operation, such as enabling or disabling a channel, adding an association or a poll, or changing an
outstation's decode level, can't be called from within a callback. They fail with the RUNTIME_CANNOT_BLOCK_WITHIN_ASYNC parameter error.
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 and when it completes you are assured there are no longer any tasks running and all of the worker threads have been joined.
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, use Runtime.set_shutdown_timeout(..) to set an upper time limit. Call this method immediately after creating the Runtime.
Shutting down the runtime using a timeout can leak memory as worker threads that haven't stopped when the timeout occurs are left running. Only use this method if you are exiting the process anyway.
Shutdown examples
- Rust
- C
- C++
- Java
- C#
Runtime shutdown is implicit in Rust when tokio::main returns.
// shutting down the runtime first stops every task and callback, including those that use the channel
dnp3_runtime_destroy(runtime);
dnp3_master_channel_destroy(channel);
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. Always 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. Always explicitly shut down the runtime instead of relying on this behavior.