runtime_named_locks ⎹ By Pieces for Developers
Overview
This Dart package provides a robust solution for managing execution flow in concurrent Dart applications through named locks, ensuring that critical sections of code are accessed in a controlled manner to prevent race conditions. Leveraging the runtime_native_semaphore package, it utilizes low-level native named semaphores, offering a reliable and efficient locking mechanism right from your Dart/Flutter Project. This approach allows for fine-grained control over resource access across multiple isolates, enhancing the safety and performance of your concurrent applications.
Use Cases
- Cross-Isolate Synchronization: Use NamedLocks to synchronize and coordinate atomic actions such as database writes, file access, or other shared resources across different Dart isolates within the same application.
- Cross-Process Thread Synchronization: In applications that span multiple processes i.e. cooperating AOTs, NamedLocks can ensure that only one process accesses a critical resource/section of code at a time, preventing race conditions and ensuring data integrity.
Platform Support
The runtime_named_locks
package supports the following platforms:
- MacOS (x86_64, arm64)
- Linux (x86_64, arm64)
- Windows (x86_64)
Installation
To add runtime_named_locks
to your Dart package, include it in your pubspec.yaml
file:
dependencies:
runtime_named_locks: ^1.0.0-beta.3
Getting Started
To get started with this Dart package, you'll primarily work with two key components: ExecutionCall
and the NamedLock.guard
static function. ExecutionCall
allows you to encapsulate the execution of a piece of code along with its expected return and exception types, providing a structured way to handle both success and failure scenarios.
Using a NamedLock to Guard a Critical Section of Code Executed by Multiple Isolates or Processes
The following example demonstrates how to use a NamedLock
to guard a "critical" section of code that is executed by multiple isolates or processes. The NamedLock.guard
method ensures that the code block is executed in a 'ATOMIC' thread/process-safe manner and is robustly protected by a lower-level native named semaphores under the hood.
import 'dart:isolate';
import 'package:runtime_native_semaphores/runtime_native_semaphores.dart' show NativeSemaphore;
void main() {
// Create a unique identifier for the semaphore
// I's suggest using an safe integer identifier from
// [safe_int_id](https://pub.dev/packages/safe_int_id)
final String name = 'my-named-lock-identifier';
spawnIsolate(name, 1);
spawnIsolate(name, 2);
// Add more isolates as needed
}
Future<void> spawnIsolate(String name, int isolate) async {
void isolateEntryPoint(SendPort sendPort) {
String name = '${safeIntId.getId()}_named_lock';
final ExecutionCall<bool, Exception> _execution = NamedLock.guard(
name: name,
execution: ExecutionCall<bool, Exception>(
callable: () {
sleep(Duration(milliseconds: Random().nextInt(5000)));
return true;
},
),
);
sendPort.send(_execution.returned);
}
final receivePort = ReceivePort();
await Isolate.spawn(isolateEntryPoint, receivePort.sendPort);
await receivePort.first;
//...
// Cleanup
receivePort.close();
}
API Reference:
Main Class
- NamedLock: Manages named locks using low-level native named semaphores to ensure thread-safe execution of code blocks across multiple isolates.
Methods:
Guard
- static ExecutionCall<T, E> guard<T, E>({required String name, required ExecutionCall<T, E> execution}): Ensures that the code block encapsulated by
ExecutionCall
is executed in a thread-safe manner, protected by a named lock to prevent race conditions.- Parameters:
- name: A
String
representing the unique name of the lock. - execution: An
ExecutionCall<R, E extends Exception>
object that encapsulates the code block to be executed safely under the named lock.- R: The return type of the code block.
- E: The type of exception that may be thrown during execution.
- callable: A
ExecutionCallType<R, E extends Exception> = R Function()
callback that contains the code block to be executed within the named lock. - safe (optional): A
bool
flag indicating whether the execution should handle exceptions internally. If set totrue
, theExecutionCall
object will capture and store any exceptions thrown during execution otherwise the exception will be rethrown to the outer scope.
- name: A
- Returns: An
ExecutionCall<R, E extends Exception>
object containing the result or the caught exception of the execution. This method ensures thread-safe execution using the named lock mechanism.
- Parameters:
Properties:
- ExecutionCall Properties:
- successful: A record
({bool isSet, bool? get})
indicating whether the execution within the guard was successful. Thesuccessful.get
field indicates whether the execution was successful, whilesuccessful.isSet
indicates whether the property has been set. - error: A record
({bool isSet, ExecutionCallErrors<R, E>? get})
that encapsulates detailed information about the execution failure, if any. This record includes:- anticipated: An optional record field
({bool isSet, E? get})
that holds the expected exception if the execution fails. - unknown: An optional record field
({bool isSet, E? get})
that may contain an unexpected exception, distinct from the anticipated type. - trace: A optional record field
({bool isSet, StackTrace? get})
providing the stack trace associated with the exception.
- anticipated: An optional record field
- returned: The result of the execution if it completes successfully. This is of type
R
, as specified in theExecutionCall
. - completer: A
Completer<R>
object that can be used to manage the completion of the execution and await the result of asynchronouscallable
callbacks.
- successful: A record
Error Handling:
- The
error
property ofExecutionCall
is a record type that provides a structured way to access information about exceptions that may occur during execution. It allows for differentiated handling of anticipated exceptions versus unexpected ones, and it includes the stack trace for in-depth debugging. Therethrow_
method can be used to rethrow the caught exception, facilitating flexible error handling strategies.
Motivation
The motivation behind this Dart package stems from the complexities and challenges associated with directly managing low-level native named semaphores, especially in a concurrent programming context. Native named semaphores offer powerful synchronization primitives but working with them directly can be cumbersome and error-prone due to the nuances of semaphore lifecycle management, cross-platform inconsistencies, and the intricacies of ensuring thread safety. Our package abstracts these complexities through a higher-level API, providing developers with an intuitive and easy-to-use interface for concurrency management. By encapsulating the low-level operations and handling the subtle nuances of working with native named semaphores, our package allows developers to focus on the logic of their applications, ensuring efficient and safe access to shared resources without getting bogged down by the underlying concurrency mechanisms. This approach not only enhances developer productivity but also improves the reliability and performance of concurrent Dart applications. Further, this runtime_named_locks
package services the demand for efficient, reliable inter-process communication (IPC) mechanisms in high-performance software, specifically within the projects like the DCLI framework and the Pieces for Developers | Flutter Desktop App while being easy to consume and integrate into existing codebases.
Contributing
We welcome any and all feedback and contributions to the runtime_named_locks
package. If you encounter any issues, have feature requests, or would like to contribute to the
package, please feel free to open an issue or submit a pull request on the GitHub repository.
License
This is an open-source package developed by the team at Pieces for Developers and is licensed under the Apache License 2.0.