A locking mechanism for Dart analogous to Java synchronized
blocks. By wrapping asynchronous
Dart code with synchronized()
, you can ensure mutually-exclusive, sequential execution of
otherwise parallel operations.
The design of this package was inspired by package
synchronized.
In fact, the API provided by synchronized_lite
is compatible with synchronized
and can be used
as a drop-in replacement, except:
- It does not support reenterant locks;
- It does not support timeouts.
In some applications, it can be possible to call an asynchronous function repeatedly, which may lead to inconsistent state. The problem gets more complex when several different asynchronous functions change a shared state. I found myself implementing over an over a pattern that involves storing a future of the currently running asynchronous task and waiting on it before initiating another asynchronous task.
I discovered synchronized after implementing my
own version and found that my implementation is much simpler. I did performance tests and
concluded that it's also faster and uses less memory. I decided to
make it available for everyone who doesn't need the bells and whistles of
synchronized but wants to enjoy the benefits of
synchronized_lite
, which are:
- Simple and clear implementation. You can review the code and understand how it works in a few minutes.
- Linear complexity and minimal memory/CPU overhead.
This package will work in a Flutter app and can be useful for ensuring sequential I/O operations such as database updates.
The example below demonstrates how to ensure that an object is created in the database only once
and that incremental save()
operations produce a consistent result. Exceptions that occur inside of
the synchronized()
call get propagated to the caller.
import 'package:synchronized_lite/synchronized_lite.dart';
abstract class FirebasePersistentModel {
final _saveLock = Lock();
Map<String, dynamic> _data;
DocumentReference _docRef;
Future<DocumentReference> create() async {
return await _saveLock.synchronized(() async {
if(_docRef != null)
return _docRef;
_data = _createData();
_docRef = await _collectionRef.add(_data);
return _docRef;
});
}
Future<bool> save() async {
return await _saveLock.synchronized<bool>(() async {
if(_docRef == null)
throw Exception("Model has not been created");
Map<String, dynamic> newData = _createData();
Map<String, dynamic> changes = getChanges(newData, _data);
if(changes.length == 0)
return false;
await _docRef.setData(changes, merge: true);
_data = newData;
return true;
});
}
// implementations of _createData(), getChanges() and other details
// are omitted in this example.
}
Please note that nested synchronized()
calls on the same lock will produce a deadlock.
Such behavior is precisely what distinguishes non-reenterant locks from reenterant locks,
and synchronized_lite
locks are non-reenterant:
import 'package:synchronized_lite/synchronized_lite.dart';
main() async {
var lock = Lock();
await lock.synchronized(() async {
await lock.synchronized(() async {
// This will never be executed
print("It works!");
});
});
}
Another example of using synchronized()
is provided in the /example
folder, which uses a Lock
object as a mixin, making synchronized blocks look more Java-like, if you prefer.
If you still need more details,
synchronized package documentation
also has a thorough explanation on how synchronized()
blocks work.
Below is the entire source code of this package:
library synchronized_lite;
import 'dart:async';
class Lock {
Future _last;
Future<T> synchronized<T>(FutureOr<T> func()) async {
final prev = _last;
final completer = Completer();
_last = completer.future;
if(prev != null)
await prev;
try {
return await func();
} finally {
completer.complete();
}
}
}
I wrote unit tests that ensure synchronized_lite
works as expected. I
verified that it behaves identically to synchronized
by running the same tests against
the synchronized
implementation. Below is the output:
00:00 +0: Without synchronized(), all incrementers run concurrently
00:00 +1: With synchronized(), all incrementers run sequentially
00:00 +2: Non-async functions work correctly with synchronized()
00:00 +3: Exceptions are propagated
00:00 +4: All tests passed!