How do I protect shared data from concurrent access in EK9?

← Concurrency · Ref: Q158

EK9 uses MutexLock of T to protect shared data from concurrent access. The canonical pattern is to hold the MutexLock as a FIELD on a wrapping class, and expose access through methods that internally use MutexKey callbacks. The lock itself never escapes the wrapping class — it has a stable field-rooted home that the compiler tracks for data-race and deadlock detection.

FIELD-ON-CLASS PATTERN

Declare the MutexLock as a field with an inline initialiser:

  Counter
    lockedCount as MutexLock of Integer: MutexLock(0)

The field initialiser is the only legal MutexLock construction site (E08254/E08256 reject locals and collections of locks).

MUTEXKEY CALLBACK PATTERN

Inside each method that needs to read or update the protected value, declare a dynamic MutexKey function. The function body is the critical section; lockedItem is the protected value while the lock is held:

  read()
    <- rtn as Integer: Integer()
    accessKey <- (rtn) is MutexKey of Integer as function
      rtn :=: lockedItem
    require lockedCount.enter(accessKey)

When the callback returns, the lock is released automatically. This prevents forgetting to unlock.

ENTER VS TRYENTER

enter() blocks until the lock is available. tryEnter() attempts to acquire the lock and returns immediately with a Boolean indicating success:

  blocking <- lockedCount.enter(keyFunction)
  attempt <- lockedCount.tryEnter(keyFunction)

COPY NOT REASSIGN

Inside a MutexKey callback, use the copy operator :=: to update lockedItem, not regular assignment :=. Reassignment would leave an outside reference to the original value, bypassing the lock.

WHY FIELD-ON-CLASS

EK9's compile-time data-race detection needs every lock to have a stable field-rooted home. Locks declared as locals, returned from functions, or held inside collections defeat the static analysis. Wrapping the lock in a class also keeps the API surface small — callers see only the protected operations.

See Q117 for singleton and DI for shared components. See Q159 for parallel processing. See Q160 for the concurrency model. See Q213 for MutexLock design pattern.

Example

defines module qa.concurrency.mutex

  defines class

    //Canonical pattern: MutexLock as a field on a wrapping class.
    //The class exposes protected operations as methods; the lock
    //never escapes.
    Counter
      lockedCount as MutexLock of Integer: MutexLock(0)

      read()
        <- rtn as Integer: Integer()

        accessKey <- (rtn) is MutexKey of Integer as function
          rtn :=: lockedItem

        require lockedCount.enter(accessKey)

      increment()
        -> amount as Integer

        accessKey <- (amount) is MutexKey of Integer as function
          lockedItem :=: lockedItem + amount

        require lockedCount.enter(accessKey)

      default operator ?

  defines program

    MutexLockDemo()
      stdout <- Stdout()

      //Construct the wrapper — the MutexLock is created by the
      //class's field initialiser, not here in user code.
      counter <- Counter()
      stdout.println(`Initial count: ${counter.read()}`)

      counter.increment(5)
      stdout.println(`After +5: ${counter.read()}`)

      counter.increment(10)
      stdout.println(`After +10: ${counter.read()}`)

Common mistakes

E08256 — MutexLock must be declared as a field on a class with an inline initialiser. Constructing one as a local (here `strayLock <- MutexLock(0)`) is rejected at SYMBOL_DEFINITION (E08256). The lock must have a stable field-rooted home so the compiler can track its identity for data-race and deadlock analysis.

Incorrect:

counter <- Counter()
      strayLock <- MutexLock(0)
      assert strayLock?

Correct:

counter <- Counter()

E08254 — MutexLock cannot appear as a type argument to another generic (here `List of MutexLock`). Use one MutexLock per shared payload, held as a field, rather than a collection of locks. See ek9 -h E08254 for details.

Incorrect:

counter <- Counter()
      badLocks <- List() of MutexLock of Integer
      assert badLocks?

Correct:

counter <- Counter()
Other ways to ask this
  • How do I use MutexLock in EK9?
  • What is thread-safe data access in EK9?
  • How does MutexLock protect shared state in EK9?

Coming from another language?

Java: synchronized blocks or ReentrantLock with try/finally. Python: threading.Lock with context manager. Rust: Mutex<T> with lock() returning a guard. Go: sync.Mutex with Lock()/Unlock(). EK9: MutexLock of T held as a class field, accessed via MutexKey callback inside class methods. The field-rooted home enables compile-time data-race and deadlock detection that other languages lack.

Keywords: mutexlock, mutex, critical, thread, lock, protect, class, concurrent, section, safety, parallel, field, shared