manual split up into multiple files; documented the new concurrency system
This commit is contained in:
parent
e9dec2feed
commit
2c1f3f75f5
31 changed files with 6198 additions and 5901 deletions
197
doc/manual/locking.txt
Normal file
197
doc/manual/locking.txt
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
Guards and locks
|
||||
================
|
||||
|
||||
Apart from ``spawn`` and ``parallel`` Nim also provides all the common low level
|
||||
concurrency mechanisms like locks, atomic intristics or condition variables.
|
||||
|
||||
Nim significantly improves on the safety of these features via additional
|
||||
pragmas:
|
||||
|
||||
1) A `guard`:idx: annotation is introduced to prevent data races.
|
||||
2) Every access of a guarded memory location needs to happen in an
|
||||
appropriate `locks`:idx: statement.
|
||||
3) Locks and routines can be annotated with `lock levels`:idx: to prevent
|
||||
deadlocks at compile time.
|
||||
|
||||
Guards and the locks section
|
||||
----------------------------
|
||||
|
||||
Protecting global variables
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Global variables and object fields can be annotated via an ``guard`` pragma:
|
||||
|
||||
.. code-block:: nim
|
||||
var glock: TLock
|
||||
var gdata {.guard: glock.}: int
|
||||
|
||||
The compiler then ensures that every access of ``gdata`` is within a ``locks``
|
||||
section:
|
||||
|
||||
.. code-block:: nim
|
||||
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:
|
||||
|
||||
.. code-block:: nim
|
||||
template lock(a: TLock; body: stmt) =
|
||||
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:
|
||||
|
||||
.. code-block:: nim
|
||||
var dummyLock {.compileTime.}: int
|
||||
var atomicCounter {.guard: dummyLock.}: int
|
||||
|
||||
template atomicRead(x): expr =
|
||||
{.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. Why these are essential is
|
||||
explained in the `lock levels`_ section.
|
||||
|
||||
|
||||
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
|
||||
expressivity of the language:
|
||||
|
||||
.. code-block:: nim
|
||||
type
|
||||
ProtectedCounter = object
|
||||
v {.guard: L.}: int
|
||||
L: TLock
|
||||
|
||||
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:
|
||||
|
||||
.. code-block:: nim
|
||||
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`:idx: 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:
|
||||
|
||||
.. code-block:: nim
|
||||
{.locks: [a[i].L].}:
|
||||
inc i
|
||||
access a[i].v
|
||||
|
||||
|
||||
|
||||
Lock levels
|
||||
-----------
|
||||
|
||||
Lock levels are used to enforce a global locking order in order to prevent
|
||||
deadlocks at compile-time. A lock level is an constant integer in the range
|
||||
0..1_000. Lock level 0 means that no lock is acquired at all.
|
||||
|
||||
If a section of code holds a lock of level ``M`` than it can also acquire any
|
||||
lock of level ``N < M``. Another lock of level ``M`` cannot be acquired. Locks
|
||||
of the same level can only be acquired *at the same time* within a
|
||||
single ``locks`` section:
|
||||
|
||||
.. code-block:: nim
|
||||
var a, b: TLock[2]
|
||||
var x: TLock[1]
|
||||
# invalid locking order: TLock[1] cannot be acquired before TLock[2]:
|
||||
{.locks: [x].}:
|
||||
{.locks: [a].}:
|
||||
...
|
||||
# valid locking order: TLock[2] acquired before TLock[1]:
|
||||
{.locks: [a].}:
|
||||
{.locks: [x].}:
|
||||
...
|
||||
|
||||
# invalid locking order: TLock[2] acquired before TLock[2]:
|
||||
{.locks: [a].}:
|
||||
{.locks: [b].}:
|
||||
...
|
||||
|
||||
# valid locking order, locks of the same level acquired at the same time:
|
||||
{.locks: [a, b].}:
|
||||
...
|
||||
|
||||
|
||||
So here is how a typical multilock statement can be implemented in Nim:
|
||||
|
||||
.. code-block:: nim
|
||||
template multilock(a, b: ptr TLock; body: stmt) =
|
||||
if cast[ByteAddress](a) < cast[ByteAddress](b):
|
||||
pthread_mutex_lock(a)
|
||||
pthread_mutex_lock(b)
|
||||
else:
|
||||
pthread_mutex_lock(b)
|
||||
pthread_mutex_lock(a)
|
||||
{.locks: [a, b].}:
|
||||
try:
|
||||
body
|
||||
finally:
|
||||
pthread_mutex_unlock(a)
|
||||
pthread_mutex_unlock(b)
|
||||
|
||||
|
||||
Whole routines can also be annotated with a ``locks`` pragma that takes a lock
|
||||
level. This then means that the routine may acquire locks of up to this level.
|
||||
This is essential so that procs can be called within a ``locks`` section:
|
||||
|
||||
.. code-block:: nim
|
||||
proc p() {.locks: 3.} = discard
|
||||
|
||||
var a: TLock[4]
|
||||
{.locks: [a].}:
|
||||
# p's locklevel (3) is strictly less than a's (4) so the call is allowed:
|
||||
p()
|
||||
|
||||
|
||||
As usual ``locks`` is an inferred effect and there is a subtype
|
||||
relation: ``proc () {.locks: N.}`` is a subtype of ``proc () {.locks: M.}``
|
||||
iff (M <= N).
|
||||
Loading…
Add table
Add a link
Reference in a new issue