synchronized

repository·master·Indexed 42 days ago

https://github.com/tekartik/synchronized.dart

A Dart package providing a basic locking mechanism (Mutex) to prevent concurrent access to asynchronous code within a single isolate. It features Lock and MultiLock classes, support for reentrant locks, acquisition timeouts, and an extension to call .synchronized() directly on any object. Designed for platform neutrality across Browser, DartVM, and Flutter with zero dependencies.

Tokens
9.5K
Snippets
28
Records
49
Agent score
45%

What's inside synchronized

  1. Use synchronized to prevent concurrent access to asynchronous code

    master
    The synchronized package provides a basic lock mechanism for Dart. It is used to ensure that a specific block of asynchronous code is executed by only one caller at a time, preventing race conditions when multiple asynchronous operations attempt to access shared resources or state simultaneously.
  2. Core Purpose and Scope of synchronized

    master

    The synchronized package provides a basic lock/mutex mechanism to prevent concurrent access to asynchronous code. It is designed to offer a solution similar to critical sections and a simple synchronized API (similar to Java) to enable features like transactions.

    Key Constraints & Scope:

    • No Cross-Process/Isolate Sync: It does not solve synchronization across different processes or isolates.
    • Zero Dependencies: It relies only on the Dart SDK.
    • Platform Neutrality: It is designed to work on the Browser, DartVM, and Flutter.
    • Identity-Based Locking: The lock mechanism is based on object identity (or ==/hashCode), so be aware of potential conflicts when using certain objects (e.g., using a String object as a lock target).
  3. How MultiLock handles lock acquisition

    master

    The MultiLock class provides a mechanism to acquire multiple locks together as a single atomic operation.

    Critical Requirement: Idempotent Iterables When passing an Iterable of locks to MultiLock, the iterable must be idempotent. This means every time the iterable is traversed, it must return the exact same set of Lock instances.

    If you pass a lazy iterable that generates new lock instances on every iteration (for example, using .map() to create new objects), MultiLock will fail to provide mutual exclusion because it will attempt to acquire different locks during its internal acquisition and release phases.

    Best Practice: Always pass a stable collection, such as a list literal or a pre-computed list, to ensure the same locks are used throughout the lifecycle of the multi-lock operation.

    // CORRECT: Using a list literal (stable/idempotent)
    await MultiLock([lockA, lockB]).synchronized(() async {
      // protected code
    });
    
    // CORRECT: Using a stable mapping over existing objects
    final locks = owners.map((o) => o.lock).toList();
    await MultiLock(locks).synchronized(() async {
      // protected code
    });
    
    // INCORRECT: Lazy iterable that creates new locks every iteration
    // This will result in a silent loss of mutual exclusion.
    await MultiLock(base.map((e) => Lock())).synchronized(() async {
      // This code is NOT protected!
    });
  4. How to use Locks correctly to prevent concurrency

    master

    To effectively prevent concurrent execution, the Lock instance must be shared between calls. A per-call lock instance will exclude nothing, as each call would be using a different lock.

    Example Concept: Instead of: await Lock().synchronized(() => doWork()); (This creates a new lock every time, providing no protection)

    Use: final myLock = Lock(); await myLock.synchronized(() => doWork()); (All calls share myLock, ensuring mutual exclusion)

  5. Understand MultiLock acquisition and timeout semantics

    master

    When using MultiLock to acquire multiple locks, be aware of the following:

    1. Timeout Bounds: The timeout provided during acquisition is applied per-lock. This means the total time spent attempting to acquire all locks in a MultiLock can result in an N × timeout bound, where N is the number of locks.
    2. Partial Acquisition/Timeout: If a TimeoutException occurs during a mid-chain acquisition (i.e., some locks were acquired but a subsequent one timed out), the library correctly unwinds the chain. The locks already acquired will be released, and their state will reflect that they are no longer held (locked == false and canLock == true).
    3. Status Reporting: The locked and inLock properties of a MultiLock describe the state of the held locks. The current implementation uses an every-based logic (meaning it reports true only if all locks in the set are fully held).
  6. Handle timeouts in synchronized blocks

    master

    When calling .synchronized(), you can provide a timeout parameter.

    Behavioral Notes:

    • Waiting: If a timeout occurs while the caller is waiting for the lock, a TimeoutException is thrown. The lock is correctly released/re-serialized so other waiters can proceed.
    • Running Computation: A timeout cannot cancel a computation that is already running. If the lock has already been acquired and the user's function is executing, the timeout will not interrupt it; the computation will run to completion.
    • Error Propagation: If a TimeoutException is thrown during the wait, the user's computation is never invoked.
  7. Understand the design limitations of synchronized computations

    master

    When using synchronized, be aware of the following design boundaries:

    • No Cancellation: A computation that has already started will run to completion even if a timeout occurs. The library does not provide a mechanism to cancel an in-progress block.
    • Unbounded Waiter Queue: The waiter chain has no maximum capacity. If many callers attempt to enter a lock simultaneously, they will all be queued, which can lead to high memory usage under extreme contention.
    • Identity-based Extension Cache: When using object extensions (e.g., object.synchronized(...)), the library uses an internal cache keyed by the object's identity (via ==). If you use classes where equality is based on value rather than identity, different objects with the same value will share the same lock, potentially causing unintended mutual exclusion.
  8. Understanding the atomicity of BasicLock.synchronized

    master

    The BasicLock.synchronized method relies on a specific Dart execution invariant to ensure atomicity during the 'check-then-act' phase (reading the current lock state and then assigning the new state).

    In Dart, an async method body executes synchronously up to the first await keyword. The BasicLock.synchronized implementation performs its state check and assignment without any await calls in the critical section. This ensures that the read-modify-write operation completes on the caller's stack before control is returned to the event loop, preventing two callers from observing an unlocked state simultaneously.

    Warning for Contributors: Adding an await (e.g., for an asynchronous policy check) anywhere before the lock state is updated will silently break mutual exclusion for the entire library.

  9. Understand the difference between synchronized() and synchronizedSync()

    master

    The package provides two ways to synchronize: synchronized() and synchronizedSync().

    • synchronized(): The standard asynchronous method. It is consistent in how it delivers results and errors regardless of whether the lock was immediately available or had to be awaited.
    • synchronizedSync(): A synchronous version intended for the 'fast path' (when the lock is already free).

    Warning: synchronizedSync() has inconsistent error delivery. If the lock is free, it throws errors synchronously. If the lock is held and must wait, it returns a failed Future. This can lead to different error-handling requirements at the same call site depending on the lock state.

  10. How to use package:synchronized for async mutual exclusion

    master

    The package:synchronized provides a Lock class to serialize asynchronous work within a single Dart isolate. Using the synchronized() method on a Lock instance ensures that the provided computation runs only when no other computation holding that same lock is currently running. This is useful for protecting shared state during interleaved await sequences (e.g., read-modify-write operations).

    Key Rules:

    • Share the Lock: You must share the same Lock instance between all call sites that need to be serialized. Storing it in a final instance field, a static final field, or a top-level final is recommended. Creating a Lock() inside a method will not provide any synchronization.
    • Scope: It only synchronizes code that explicitly calls synchronized() on that specific lock. It does not protect resources from code that accesses them without using the lock.
    • Isolate Limit: It only works within a single isolate. It does not synchronize across isolates, processes, or browser tabs.
    import 'package:synchronized/synchronized.dart';
    
    final _lock = Lock();
    
    Future<void> save() => _lock.synchronized(() async {
      // Only one save() body runs at a time.
    });
  11. Understand MultiLock state semantics

    master

    A MultiLock manages a set of underlying locks. Its state properties follow these rules:

    • locked: Returns true if any of the underlying locks are currently held. This is used to indicate contention.
    • inLock: Returns true if any of the underlying locks are currently held.
    • canLock: Returns true only if all underlying locks are currently available (unlocked).

    This means that if locked is false, it implies canLock is true (the lock is fully available).

  12. Understand the behavior of synchronizedSync with Futures

    master

    The synchronizedSync method is intended for synchronous computations. There is an ongoing architectural decision regarding its contract when the provided computation returns a Future.

    Currently, if a computation returns a Future, synchronizedSync may skip the lock (returning the Future immediately without holding the lock for the duration of the asynchronous work). Users should be aware that if they require the lock to be held until a Future completes, they should use the standard synchronized method instead of synchronizedSync.