Guards and locks

Nim provides common low level concurrency mechanisms like locks, atomic intrinsics or condition variables.

Nim significantly improves on the safety of these features via additional pragmas:

  1. A guard annotation is introduced to prevent data races.
  2. Every access of a guarded memory location needs to happen in an appropriate locks statement.

Guards and locks sections

Protecting global variables

Object fields and global variables can be annotated via a guard pragma:

  1. import std/locks
  2. var glock: Lock
  3. var gdata {.guard: glock.}: int

The compiler then ensures that every access of gdata is within a locks section:

  1. proc invalid =
  2. # invalid: unguarded access:
  3. echo gdata
  4. proc valid =
  5. # valid access:
  6. {.locks: [glock].}:
  7. echo gdata

Top level accesses to gdata are always allowed so that it can be initialized conveniently. It is assumed (but not enforced) that every top level statement is executed before any concurrent action happens.

The locks section deliberately looks ugly because it has no runtime semantics and should not be used directly! It should only be used in templates that also implement some form of locking at runtime:

  1. template lock(a: Lock; body: untyped) =
  2. pthread_mutex_lock(a)
  3. {.locks: [a].}:
  4. try:
  5. body
  6. finally:
  7. pthread_mutex_unlock(a)

The guard does not need to be of any particular type. It is flexible enough to model low level lockfree mechanisms:

  1. var dummyLock {.compileTime.}: int
  2. var atomicCounter {.guard: dummyLock.}: int
  3. template atomicRead(x): untyped =
  4. {.locks: [dummyLock].}:
  5. memoryReadBarrier()
  6. x
  7. echo atomicRead(atomicCounter)

The locks pragma takes a list of lock expressions locks: [a, b, …] in order to support multi lock statements.

Protecting general locations

The guard annotation can also be used to protect fields within an object. The guard then needs to be another field within the same object or a global variable.

Since objects can reside on the heap or on the stack, this greatly enhances the expressiveness of the language:

  1. import std/locks
  2. type
  3. ProtectedCounter = object
  4. v {.guard: L.}: int
  5. L: Lock
  6. proc incCounters(counters: var openArray[ProtectedCounter]) =
  7. for i in 0..counters.high:
  8. lock counters[i].L:
  9. inc counters[i].v

The access to field x.v is allowed since its guard x.L is active. After template expansion, this amounts to:

  1. proc incCounters(counters: var openArray[ProtectedCounter]) =
  2. for i in 0..counters.high:
  3. pthread_mutex_lock(counters[i].L)
  4. {.locks: [counters[i].L].}:
  5. try:
  6. inc counters[i].v
  7. finally:
  8. pthread_mutex_unlock(counters[i].L)

There is an analysis that checks that counters[i].L is the lock that corresponds to the protected location counters[i].v. This analysis is called path analysis because it deals with paths to locations like obj.field[i].fieldB[j].

The path analysis is currently unsound, but that doesn’t make it useless. Two paths are considered equivalent if they are syntactically the same.

This means the following compiles (for now) even though it really should not:

  1. {.locks: [a[i].L].}:
  2. inc i
  3. access a[i].v