synchronized_lite 1.0.0+2 copy "synchronized_lite: ^1.0.0+2" to clipboard
synchronized_lite: ^1.0.0+2 copied to clipboard

A locking mechanism for Dart analogous to synchronized blocks in Java.

synchronized_lite #

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.

Motivation #

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.

Usage #

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.

Implementation #

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();
    }
  }

}

Testing #

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!
1
likes
40
pub points
10%
popularity

Publisher

unverified uploader

A locking mechanism for Dart analogous to synchronized blocks in Java.

Repository (GitHub)
View/report issues

License

MIT (LICENSE)

More

Packages that depend on synchronized_lite