synchronized

3.5.0

Lock mechanism to prevent concurrent access to asynchronous code.

Download archive

Readme

synchronized

Basic lock mechanism to prevent concurrent access to asynchronous code.

pub package

Goal

You were missing hard to debug deadlocks, here it is!

The goal is to propose a solution similar to critical sections and offer a simple synchronized API à la Java style. It provides a basic Lock/Mutex solution to allow features like transactions.

The name is biased as we are single threaded in Dart. However since we write asychronous code (await) like we would write synchronous code, it makes the overall API feel the same.

The goal is to ensure for a single process (single isolate) that some asynchronous operations can run without conflict. It won't solve cross-process (or cross-isolate) synchronization.

For single process (single isolate) accessing some resources (database..), it can help to

  • Provide transaction on database system that don't have transaction mechanism (mongodb, file system)
  • In html application make sure some asynchronous UI operation are not conflicting (login, transition)

Feature

  • By default a lock is not reentrant
  • Timeout support
  • Support for reentrant lock (using Zone)
  • Consistent behavior (i.e. if it is unlocked calling synchronized grab the lock)
  • Values and Errors are properly reported to the caller
  • Work on Browser, DartVM and Flutter
  • No dependencies (other than the sdk itself)

It differs from the pool package used with a resource count of 1 by supporting a reentrant option

Usage

A simple usage example:

import 'package:synchronized/synchronized.dart';

Future<void> main() async {
  // Use this object to prevent concurrent access to data
  var lock = Lock();
  // ...
  await lock.synchronized(() async {
    // Only this block can run (once) until done
    // ...
  });
}

If you need a re-entrant lock you can use

var lock = Lock(reentrant: true);
// ...
await lock.synchronized(() async {
  // do some stuff
  // ...

  await lock.synchronized(() async {
    // other stuff
  });
});

A basic lock is not reentrant by default and does not use Zone. It behaves like an async executor with a pool capacity of 1

var lock = Lock();
// ...
lock.synchronized(() async {
  // do some stuff
  // ...
});

The return value is preserved

int value = await lock.synchronized(() {
  return 1;
});

Using the package:synchronized/extension.dart import, you can turn any object into a lock. synchronized() can then be called on any object

import 'package:synchronized/extension.dart';

class MyClass {

  /// Perform a long action that won't be called more than one at a time.
  Future<void> performAction() {
    // Lock at the instance level
    return synchronized(() async {
      // ...uninterrupted action
    });
  }
}

How it works

The next tasks is executed once the previous one is done

Re-entrant locks uses Zone to know in which context a block is running in order to be reentrant. It maintains a list of inner tasks to be awaited for.

Example

Consider the following dummy code

Future<void> writeSlow(int value) async {
  await Future<void>.delayed(const Duration(milliseconds: 1));
  stdout.write(value);
}

Future<void> write(List<int> values) async {
  for (var value in values) {
    await writeSlow(value);
  }
}

Future<void> write1234() async {
  await write([1, 2, 3, 4]);
}

Doing

write1234();
write1234();

would print

11223344

while doing

lock.synchronized(write1234);
lock.synchronized(write1234);

would print

12341234

The Lock instance

Have in mind that the Lock instance must be shared between calls in order to effectively prevent concurrent execution. For instance, in the example below the lock instance is the same between all myMethod() calls.

class MyClass {
  final _lock = Lock();

  Future<void> myMethod() async {
    await _lock.synchronized(() async {
      step1();
      step2();
      step3();
    });
  }
}

Typically you would create a global or static instance Lock to prevent concurrent access to a global resource or a class instance Lock to prevent concurrent modifications of class instance data and resources.

Timeout

synchronized() accepts an optional timeout:

try {
  await lock.synchronized(() async {
    // ...
  }, timeout: const Duration(seconds: 1));
} on TimeoutException catch (_) {
  // The lock could not be acquired in time.
}

The timeout bounds acquiring the lock, not running the computation. If it expires, the computation is never called and a TimeoutException is thrown. Once the computation has started it runs to completion: there is no cancellation, so a computation may well outlive the timeout that let it start.

For a MultiLock the timeout is the budget for acquiring the whole set, not for each member lock in turn.

Locking on any object

With package:synchronized/extension.dart, the lock associated with an object is found by equality (==/hashCode), not by identity, and the cache is global to the process. Two objects that compare equal therefore share one lock, which makes value types such as String and int a namespace shared with every other library in the application:

// These contend with each other, and with any other library
// that happens to lock on the same text.
await 'my-key'.synchronized(() async { /* ... */ });

When you want isolation, lock on a private instance instead:

class MyClass {
  final _lock = Object();

  Future<void> myMethod() => _lock.synchronized(() async {
        // ...
      });
}

locked/inLock/canLock status

locked

For basic and reentrant lock, locked returns whether the lock is currently locked.

For multi lock, it returns true if all inner locks locked values are true.

inLock

For reentrant locks, inLock returns whether the current zone is locked by the lock. i.e. it is true if the current block is running inside a synchronized block of the lock.

For basic lock, it matches the locked property and since it does not mean anything, it should not be used as behavior may change in the future.

For multi lock, it returns true if all inner locks inLock values are true.

canLock

canLock returns whether the lock can be locked immediately.

For basic lock, it is true if the lock is not locked.

For reentrant lock, it is true if the current zone lock level is free: outside of the lock, it can be false right after a block completed while other calls are still queued, even if locked is already false.

For multi lock, it returns true if all inner locks canLock values are true.

MultiLock

As of version 3.3.0, a MultiLock is available. It is a lock that can be used to synchronize multiple locks at once.

var lock1 = Lock();
var lock2 = Lock();
var multiLock = MultiLock(locks: [lock1, lock2]);

multiLock.synchronized(() async {
  // lock1 and lock2 are locked at this point
  ...
});

Features and bugs

Please feel free to:

Changelog

Changelog

3.5.0

  • Fix ReentrantLock.canLock reporting the lock free right after a synchronous block while other calls were still queued, which let synchronizedSync run before earlier synchronized calls. In that case canLock is now false and synchronizedSync returns a Future that completes in order.
  • Example: readme2 was never called and now uses canLock

3.4.2

  • Add synchronized-lock agent skill (skills/), installable with dart run skills@ get

3.4.1+2

  • Requires dart 3.12
  • Improve documentation
  • MultiLock timeout now bounds the whole acquisition (one shared budget) instead of each underlying lock
  • MultiLock copies its locks at construction
  • synchronizedSync asserts (in debug mode) when the computation returns a Future
  • ReentrantLock.canLock throws a StateError (instead of a RangeError, or true) when called from a zone that outlived its synchronized block
  • Add MultiLock.toString

3.4.0+1

  • Requires dart 3.11

3.4.0

  • Requires dart 3.8
  • Add synchronizedSync for synchronous computation that returns a FutureOr

3.3.1

  • Required dart 3.7

3.3.0+3

  • Add MultiLock (thanks to @saibotma)

3.2.0

  • Required dart 3.5

3.1.0+1

  • Dart 3 support

3.0.1

  • strict-casts and sdk 2.18 support

3.0.0+3

  • Add MIT licence

3.0.0+2

  • null safety support, breaking change

2.2.0+2

  • Add SynchronizedLock extension to turn any object into a lock.

2.1.1

  • pedantic 1.9 support

2.1.0+3

  • Fix reentrant lock to properly synchronize inner blocks

2.0.0

  • Remove previously deprecated SynchronizedLock and synchronized function

1.5.2

  • Remove dev_test dependency

1.5.1

  • Dart2 stable support

1.5.0

  • Deprecate SynchronizedLock and synchronized

1.4.0

  • Re-use non-reentrant lock in synchronized method

1.3.0

  • Add non-reentrant lock that do not use Zone

1.2.1

  • implicit-casts: false (testing dart2 support)

1.2.0

  • Use generic instead of 2.0 deprecated comments

1.1.0

  • Fix inner task issue, next outer task will wait for all inner tasks to terminate
  • Properly handle nested thrown error

1.0.0

  • Bump to version 1.0.0

0.1.0

  • Initial version

Installing

export PUB_HOSTED_URL=https://pub.mirror.p-hatan.com
flutter pub add synchronized
pubspec.yaml
name: synchronized
description: Lock mechanism to prevent concurrent access to asynchronous code.
version: 3.5.0
homepage: https://github.com/tekartik/synchronized.dart/tree/master/synchronized
funding:
  - https://github.com/sponsors/alextekartik

topics:
  - mutex
  - lock
  - async
  - concurrency

environment:
  sdk: ^3.12.0

dev_dependencies:
  web: '>=0.5.0'
  lints: '>=1.0.1'
  pub_semver: '>=2.0.0'
  test: '>=1.16.2'
  build_runner: '>=1.11.2'
  build_test: '>=1.3.7'
  build_web_compilers: '>=2.16.4'
  process_run: '>=0.12.0-0'
  yaml: '>=3.0.0'

Version history

VersionPublishedArchiveSHA-256
0.1.0On demandDownloadf79a2201…177870
1.0.0On demandDownloadf013d5d6…99294b
1.0.1On demandDownload42281ebd…5ae094
1.0.2On demandDownload54d62393…dc5a7e
1.1.0On demandDownload8f0dab5e…60d5e7
1.2.0On demandDownloadaafb9d0d…0c0e70
1.2.1On demandDownload7c7e786f…64f4ea
1.3.0On demandDownloadee06e007…f75d1b
1.4.0On demandDownloade2c41767…d209f8
1.5.0-dev.2On demandDownload2ee0b4ba…3327ae
1.5.0On demandDownload6dd37fd1…747caa
1.5.0+1On demandDownloaddbed62e3…40cd0b
1.5.1On demandDownload13839638…1ef0c4
1.5.1+1On demandDownload53f36159…b64352
1.5.2On demandDownload8ab2fab7…b18eac
1.5.3On demandDownloadc652be25…d0aa1d
1.5.3+1On demandDownload13fffd90…658630
1.5.3+2On demandDownload7c84434c…93fe67
2.0.0On demandDownloadb0b6eeb6…922b9c
2.0.1On demandDownloadb9051c3d…ab4b45
2.0.2On demandDownload4542c7d3…0a62d4
2.0.2+1On demandDownload36fe1ff6…a3da95
2.1.0On demandDownload886b9573…90f760
2.1.0+1On demandDownloadde067bff…9ba9f2
2.1.0+2On demandDownload5a92f740…06fe75
2.1.1On demandDownloada73e0ff1…2be1b3
2.2.0On demandDownload3aec6b87…ba759c
2.2.0+1On demandDownload70f1e8fe…d793be
2.2.0+2On demandDownload294228ee…88f735
3.0.0-nullsafety.0On demandDownloadc2e5ef96…971497
3.0.0-nullsafety.1On demandDownload1bce1e57…6f1ab1
3.0.0On demandDownload271977ff…d3ef83
3.0.0+1On demandDownloada4c404da…a9f3df
3.0.0+2On demandDownloada7f07909…c09dc2
3.0.0+3On demandDownload7b530acd…ade1ac
3.0.1On demandDownload33b31b6b…aaf04b
3.1.0On demandDownload5fcbd276…a02c60
3.1.0+1On demandDownload539ef412…73d558
3.2.0On demandDownloada824e842…371255
3.3.0On demandDownload977a21ae…8c9e16
3.3.0+1On demandDownload64797979…fd250e
3.3.0+2On demandDownload51b08572…55d7bc
3.3.0+3On demandDownload69fe30f3…cf9225
3.3.1On demandDownload0669c70f…bc0ef6
3.4.0On demandDownloadc254ade2…efd7c0
3.4.0+1On demandDownload63896c27…c7c9e5
3.4.1On demandDownload93b153dc…456153
3.4.1+1On demandDownload61894a19…a7a423
3.4.1+2On demandDownload3a7b5d17…046931
3.4.2On demandDownload397b146f…490b2d
3.5.0Cached · Upstream SHA-256 matchedDownloadb3d516db…e657c5

Dependencies

No direct dependencies declared.