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:
- A guard annotation is introduced to prevent data races.
- 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:
import std/locks
var glock: Lock
var gdata {.guard: glock.}: int
The compiler then ensures that every access of gdata is within a locks section:
proc invalid =
# invalid: unguarded access:
echo gdata
proc valid =
# valid access:
{.locks: [glock].}:
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:
template lock(a: Lock; body: untyped) =
pthread_mutex_lock(a)
{.locks: [a].}:
try:
body
finally:
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:
var dummyLock {.compileTime.}: int
var atomicCounter {.guard: dummyLock.}: int
template atomicRead(x): untyped =
{.locks: [dummyLock].}:
memoryReadBarrier()
x
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:
import std/locks
type
ProtectedCounter = object
v {.guard: L.}: int
L: Lock
proc incCounters(counters: var openArray[ProtectedCounter]) =
for i in 0..counters.high:
lock counters[i].L:
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:
proc incCounters(counters: var openArray[ProtectedCounter]) =
for i in 0..counters.high:
pthread_mutex_lock(counters[i].L)
{.locks: [counters[i].L].}:
try:
inc counters[i].v
finally:
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:
{.locks: [a[i].L].}:
inc i
access a[i].v