I got E08252 NESTED_ENTER_DIFFERENT_LOCK — how does nesting two locks form a deadlock cycle?

← Concurrency · Ref: Q1351

E08252 fires when EK9's deadlock detector finds two code paths that nest the same pair of distinct locks in OPPOSITE orders — one acquires lockA then lockB, another acquires lockB then lockA. Two threads running these paths concurrently can each hold one lock and wait forever for the other.

WHAT THE ERROR MEANS

Every `lockA.enter(...)` with a `lockB.enter(...)` nested inside records an edge lockA -> lockB in a workspace-wide precedence graph. When updateA nests getB() inside getA() (edge A->B) and updateB nests getA() inside getB() (edge B->A), the graph has a cycle A->B->A — a deadlock.

HOW TO FIX

Never hold two locks at once when the acquisition order can contradict. Acquire them one at a time (release the first before taking the second), or unify the protected state under a single MutexLock of a record.

  require pair.getA().enter(keyA)   //completes and releases
  require pair.getB().enter(keyB)   //independent — no nesting

WHY EK9 DETECTS THIS

Lock-order cycles only deadlock under specific interleavings, so they slip through testing. EK9 builds the precedence graph at compile time (the model the Linux kernel's lockdep uses at runtime) and refuses to compile any cycle.

Example

defines module qa.concurrency.nested.enter.different

  defines class

    LockPair
      lockA as MutexLock of Integer: MutexLock(Integer(0))
      lockB as MutexLock of Integer: MutexLock(Integer(0))
      getA()
        <- rtn as MutexLock of Integer: lockA
      getB()
        <- rtn as MutexLock of Integer: lockB
      default operator ?

  defines function

    updateA()
      -> pair as LockPair
      keyA <- () is MutexKey of Integer as function
        stdout <- Stdout()
        stdout.println(lockedItem)
      require pair.getA().enter(keyA)

    updateB()
      -> pair as LockPair
      keyB <- () is MutexKey of Integer as function
        stdout <- Stdout()
        stdout.println(lockedItem)
      require pair.getB().enter(keyB)

Common mistakes

E08252 — updateA nests getB() inside getA() (edge A->B) while updateB nests getA() inside getB() (edge B->A), forming a cycle A->B->A — a runtime deadlock. Fix by acquiring the locks sequentially (no nesting) or unifying them under one MutexLock of a record. See ek9 -h E08252.

Incorrect:

updateA()
      -> pair as LockPair
      innerA <- () is MutexKey of Integer as function
        stdout <- Stdout()
        stdout.println(lockedItem)
      outerA <- (pair, innerA) is MutexKey of Integer as function
        require pair.getB().enter(innerA)
      require pair.getA().enter(outerA)

    updateB()
      -> pair as LockPair
      innerB <- () is MutexKey of Integer as function
        stdout <- Stdout()
        stdout.println(lockedItem)
      outerB <- (pair, innerB) is MutexKey of Integer as function
        require pair.getA().enter(innerB)
      require pair.getB().enter(outerB)

Correct:

updateA()
      -> pair as LockPair
      keyA <- () is MutexKey of Integer as function
        stdout <- Stdout()
        stdout.println(lockedItem)
      require pair.getA().enter(keyA)

    updateB()
      -> pair as LockPair
      keyB <- () is MutexKey of Integer as function
        stdout <- Stdout()
        stdout.println(lockedItem)
      require pair.getB().enter(keyB)
Other ways to ask this
  • Why does nesting two MutexLocks fail in EK9?
  • E08252 nested enter different lock fix
  • lock-order cycle from getA/getB nesting
  • two locks acquired in opposite order EK9

Coming from another language?

Java/C#: lock-order deadlocks are found at runtime via thread dumps; static tools catch only simple cases. Go: the runtime detector fires only on total deadlock. EK9: workspace-wide static cycle detection on the lock precedence graph, rejected at compile time.

Keywords: lock, enter, order, cycle, E08252, deadlock, concurrent, precedence, getB, getA, MutexLock, nested