mirror of
https://github.com/nim-lang/Nim.git
synced 2026-07-18 23:11:36 +00:00
move more stuff into manual_experimental
This commit is contained in:
386
doc/manual.rst
386
doc/manual.rst
@@ -6842,15 +6842,14 @@ To enable thread support the ``--threads:on`` command line switch needs to
|
||||
be used. The ``system`` module then contains several threading primitives.
|
||||
See the `threads <threads.html>`_ and `channels <channels.html>`_ modules
|
||||
for the low level thread API. There are also high level parallelism constructs
|
||||
available. See `spawn <#parallel-amp-spawn>`_ for further details.
|
||||
available. See `spawn <manual_experimental.html#parallel-amp-spawn>`_ for
|
||||
further details.
|
||||
|
||||
Nim's memory model for threads is quite different than that of other common
|
||||
programming languages (C, Pascal, Java): Each thread has its own (garbage
|
||||
collected) heap and sharing of memory is restricted to global variables. This
|
||||
helps to prevent race conditions. GC efficiency is improved quite a lot,
|
||||
because the GC never has to stop other threads and see what they reference.
|
||||
Memory allocation requires no lock at all! This design easily scales to massive
|
||||
multicore processors that are becoming the norm.
|
||||
|
||||
|
||||
Thread pragma
|
||||
@@ -6876,9 +6875,8 @@ directly or indirectly through a call to a GC unsafe proc.
|
||||
The `gcsafe`:idx: annotation can be used to mark a proc to be gcsafe,
|
||||
otherwise this property is inferred by the compiler. Note that ``noSideEffect``
|
||||
implies ``gcsafe``. The only way to create a thread is via ``spawn`` or
|
||||
``createThread``. ``spawn`` is usually the preferable method. Either way
|
||||
the invoked proc must not use ``var`` parameters nor must any of its parameters
|
||||
contain a ``ref`` or ``closure`` type. This enforces
|
||||
``createThread``. The invoked proc must not use ``var`` parameters nor must
|
||||
any of its parameters contain a ``ref`` or ``closure`` type. This enforces
|
||||
the *no heap sharing restriction*.
|
||||
|
||||
Routines that are imported from C are always assumed to be ``gcsafe``.
|
||||
@@ -6928,379 +6926,3 @@ in one thread cannot affect any other thread. However, an *unhandled* exception
|
||||
in one thread terminates the whole *process*!
|
||||
|
||||
|
||||
|
||||
Parallel & Spawn
|
||||
================
|
||||
|
||||
Nim has two flavors of parallelism:
|
||||
1) `Structured`:idx: parallelism via the ``parallel`` statement.
|
||||
2) `Unstructured`:idx: parallelism via the standalone ``spawn`` statement.
|
||||
|
||||
Nim has a builtin thread pool that can be used for CPU intensive tasks. For
|
||||
IO intensive tasks the ``async`` and ``await`` features should be
|
||||
used instead. Both parallel and spawn need the `threadpool <threadpool.html>`_
|
||||
module to work.
|
||||
|
||||
Somewhat confusingly, ``spawn`` is also used in the ``parallel`` statement
|
||||
with slightly different semantics. ``spawn`` always takes a call expression of
|
||||
the form ``f(a, ...)``. Let ``T`` be ``f``'s return type. If ``T`` is ``void``
|
||||
then ``spawn``'s return type is also ``void`` otherwise it is ``FlowVar[T]``.
|
||||
|
||||
Within a ``parallel`` section sometimes the ``FlowVar[T]`` is eliminated
|
||||
to ``T``. This happens when ``T`` does not contain any GC'ed memory.
|
||||
The compiler can ensure the location in ``location = spawn f(...)`` is not
|
||||
read prematurely within a ``parallel`` section and so there is no need for
|
||||
the overhead of an indirection via ``FlowVar[T]`` to ensure correctness.
|
||||
|
||||
**Note**: Currently exceptions are not propagated between ``spawn``'ed tasks!
|
||||
|
||||
|
||||
Spawn statement
|
||||
---------------
|
||||
|
||||
`spawn`:idx: can be used to pass a task to the thread pool:
|
||||
|
||||
.. code-block:: nim
|
||||
import threadpool
|
||||
|
||||
proc processLine(line: string) =
|
||||
discard "do some heavy lifting here"
|
||||
|
||||
for x in lines("myinput.txt"):
|
||||
spawn processLine(x)
|
||||
sync()
|
||||
|
||||
For reasons of type safety and implementation simplicity the expression
|
||||
that ``spawn`` takes is restricted:
|
||||
|
||||
* It must be a call expression ``f(a, ...)``.
|
||||
* ``f`` must be ``gcsafe``.
|
||||
* ``f`` must not have the calling convention ``closure``.
|
||||
* ``f``'s parameters may not be of type ``var``.
|
||||
This means one has to use raw ``ptr``'s for data passing reminding the
|
||||
programmer to be careful.
|
||||
* ``ref`` parameters are deeply copied which is a subtle semantic change and
|
||||
can cause performance problems but ensures memory safety. This deep copy
|
||||
is performed via ``system.deepCopy`` and so can be overridden.
|
||||
* For *safe* data exchange between ``f`` and the caller a global ``TChannel``
|
||||
needs to be used. However, since spawn can return a result, often no further
|
||||
communication is required.
|
||||
|
||||
|
||||
``spawn`` executes the passed expression on the thread pool and returns
|
||||
a `data flow variable`:idx: ``FlowVar[T]`` that can be read from. The reading
|
||||
with the ``^`` operator is **blocking**. However, one can use ``blockUntilAny`` to
|
||||
wait on multiple flow variables at the same time:
|
||||
|
||||
.. code-block:: nim
|
||||
import threadpool, ...
|
||||
|
||||
# wait until 2 out of 3 servers received the update:
|
||||
proc main =
|
||||
var responses = newSeq[FlowVarBase](3)
|
||||
for i in 0..2:
|
||||
responses[i] = spawn tellServer(Update, "key", "value")
|
||||
var index = blockUntilAny(responses)
|
||||
assert index >= 0
|
||||
responses.del(index)
|
||||
discard blockUntilAny(responses)
|
||||
|
||||
Data flow variables ensure that no data races
|
||||
are possible. Due to technical limitations not every type ``T`` is possible in
|
||||
a data flow variable: ``T`` has to be of the type ``ref``, ``string``, ``seq``
|
||||
or of a type that doesn't contain a type that is garbage collected. This
|
||||
restriction is not hard to work-around in practice.
|
||||
|
||||
|
||||
|
||||
Parallel statement
|
||||
------------------
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: nim
|
||||
:test: "nim c --threads:on $1"
|
||||
|
||||
# Compute PI in an inefficient way
|
||||
import strutils, math, threadpool
|
||||
{.experimental: "parallel".}
|
||||
|
||||
proc term(k: float): float = 4 * math.pow(-1, k) / (2*k + 1)
|
||||
|
||||
proc pi(n: int): float =
|
||||
var ch = newSeq[float](n+1)
|
||||
parallel:
|
||||
for k in 0..ch.high:
|
||||
ch[k] = spawn term(float(k))
|
||||
for k in 0..ch.high:
|
||||
result += ch[k]
|
||||
|
||||
echo formatFloat(pi(5000))
|
||||
|
||||
|
||||
The parallel statement is the preferred mechanism to introduce parallelism in a
|
||||
Nim program. A subset of the Nim language is valid within a ``parallel``
|
||||
section. This subset is checked during semantic analysis to be free of data
|
||||
races. A sophisticated `disjoint checker`:idx: ensures that no data races are
|
||||
possible even though shared memory is extensively supported!
|
||||
|
||||
The subset is in fact the full language with the following
|
||||
restrictions / changes:
|
||||
|
||||
* ``spawn`` within a ``parallel`` section has special semantics.
|
||||
* Every location of the form ``a[i]`` and ``a[i..j]`` and ``dest`` where
|
||||
``dest`` is part of the pattern ``dest = spawn f(...)`` has to be
|
||||
provably disjoint. This is called the *disjoint check*.
|
||||
* Every other complex location ``loc`` that is used in a spawned
|
||||
proc (``spawn f(loc)``) has to be immutable for the duration of
|
||||
the ``parallel`` section. This is called the *immutability check*. Currently
|
||||
it is not specified what exactly "complex location" means. We need to make
|
||||
this an optimization!
|
||||
* Every array access has to be provably within bounds. This is called
|
||||
the *bounds check*.
|
||||
* Slices are optimized so that no copy is performed. This optimization is not
|
||||
yet performed for ordinary slices outside of a ``parallel`` section.
|
||||
|
||||
|
||||
Guards and locks
|
||||
================
|
||||
|
||||
Apart from ``spawn`` and ``parallel`` Nim also provides all the 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`: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 allow
|
||||
potential deadlocks to be detected during semantic analysis.
|
||||
|
||||
|
||||
Guards and the locks section
|
||||
----------------------------
|
||||
|
||||
Protecting global variables
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Object fields and global variables can be annotated via a ``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: 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:
|
||||
|
||||
.. code-block:: nim
|
||||
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. Why these are essential is
|
||||
explained in the `lock levels <#guards-and-locks-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 detect
|
||||
potential deadlocks during semantic analysis. 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].}:
|
||||
...
|
||||
|
||||
|
||||
Here is how a typical multilock statement can be implemented in Nim. Note how
|
||||
the runtime check is required to ensure a global ordering for two locks ``a``
|
||||
and ``b`` of the same lock level:
|
||||
|
||||
.. code-block:: nim
|
||||
template multilock(a, b: ptr TLock; body: untyped) =
|
||||
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).
|
||||
|
||||
The ``locks`` pragma can also take the special value ``"unknown"``. This
|
||||
is useful in the context of dynamic method dispatching. In the following
|
||||
example, the compiler can infer a lock level of 0 for the ``base`` case.
|
||||
However, one of the overloaded methods calls a procvar which is
|
||||
potentially locking. Thus, the lock level of calling ``g.testMethod``
|
||||
cannot be inferred statically, leading to compiler warnings. By using
|
||||
``{.locks: "unknown".}``, the base method can be marked explicitly as
|
||||
having unknown lock level as well:
|
||||
|
||||
.. code-block:: nim
|
||||
type SomeBase* = ref object of RootObj
|
||||
type SomeDerived* = ref object of SomeBase
|
||||
memberProc*: proc ()
|
||||
|
||||
method testMethod(g: SomeBase) {.base, locks: "unknown".} = discard
|
||||
method testMethod(g: SomeDerived) =
|
||||
if g.memberProc != nil:
|
||||
g.memberProc()
|
||||
|
||||
|
||||
Taint mode
|
||||
==========
|
||||
|
||||
The Nim compiler and most parts of the standard library support
|
||||
a taint mode. Input strings are declared with the `TaintedString`:idx:
|
||||
string type declared in the ``system`` module.
|
||||
|
||||
If the taint mode is turned on (via the ``--taintMode:on`` command line
|
||||
option) it is a distinct string type which helps to detect input
|
||||
validation errors:
|
||||
|
||||
.. code-block:: nim
|
||||
echo "your name: "
|
||||
var name: TaintedString = stdin.readline
|
||||
# it is safe here to output the name without any input validation, so
|
||||
# we simply convert `name` to string to make the compiler happy:
|
||||
echo "hi, ", name.string
|
||||
|
||||
If the taint mode is turned off, ``TaintedString`` is simply an alias for
|
||||
``string``.
|
||||
|
||||
@@ -1230,3 +1230,379 @@ constraints affect ordinary overloading resolution then:
|
||||
However, the constraints ``alias`` and ``noalias`` are not available in
|
||||
ordinary routines.
|
||||
|
||||
|
||||
Parallel & Spawn
|
||||
================
|
||||
|
||||
Nim has two flavors of parallelism:
|
||||
1) `Structured`:idx: parallelism via the ``parallel`` statement.
|
||||
2) `Unstructured`:idx: parallelism via the standalone ``spawn`` statement.
|
||||
|
||||
Nim has a builtin thread pool that can be used for CPU intensive tasks. For
|
||||
IO intensive tasks the ``async`` and ``await`` features should be
|
||||
used instead. Both parallel and spawn need the `threadpool <threadpool.html>`_
|
||||
module to work.
|
||||
|
||||
Somewhat confusingly, ``spawn`` is also used in the ``parallel`` statement
|
||||
with slightly different semantics. ``spawn`` always takes a call expression of
|
||||
the form ``f(a, ...)``. Let ``T`` be ``f``'s return type. If ``T`` is ``void``
|
||||
then ``spawn``'s return type is also ``void`` otherwise it is ``FlowVar[T]``.
|
||||
|
||||
Within a ``parallel`` section sometimes the ``FlowVar[T]`` is eliminated
|
||||
to ``T``. This happens when ``T`` does not contain any GC'ed memory.
|
||||
The compiler can ensure the location in ``location = spawn f(...)`` is not
|
||||
read prematurely within a ``parallel`` section and so there is no need for
|
||||
the overhead of an indirection via ``FlowVar[T]`` to ensure correctness.
|
||||
|
||||
**Note**: Currently exceptions are not propagated between ``spawn``'ed tasks!
|
||||
|
||||
|
||||
Spawn statement
|
||||
---------------
|
||||
|
||||
`spawn`:idx: can be used to pass a task to the thread pool:
|
||||
|
||||
.. code-block:: nim
|
||||
import threadpool
|
||||
|
||||
proc processLine(line: string) =
|
||||
discard "do some heavy lifting here"
|
||||
|
||||
for x in lines("myinput.txt"):
|
||||
spawn processLine(x)
|
||||
sync()
|
||||
|
||||
For reasons of type safety and implementation simplicity the expression
|
||||
that ``spawn`` takes is restricted:
|
||||
|
||||
* It must be a call expression ``f(a, ...)``.
|
||||
* ``f`` must be ``gcsafe``.
|
||||
* ``f`` must not have the calling convention ``closure``.
|
||||
* ``f``'s parameters may not be of type ``var``.
|
||||
This means one has to use raw ``ptr``'s for data passing reminding the
|
||||
programmer to be careful.
|
||||
* ``ref`` parameters are deeply copied which is a subtle semantic change and
|
||||
can cause performance problems but ensures memory safety. This deep copy
|
||||
is performed via ``system.deepCopy`` and so can be overridden.
|
||||
* For *safe* data exchange between ``f`` and the caller a global ``TChannel``
|
||||
needs to be used. However, since spawn can return a result, often no further
|
||||
communication is required.
|
||||
|
||||
|
||||
``spawn`` executes the passed expression on the thread pool and returns
|
||||
a `data flow variable`:idx: ``FlowVar[T]`` that can be read from. The reading
|
||||
with the ``^`` operator is **blocking**. However, one can use ``blockUntilAny`` to
|
||||
wait on multiple flow variables at the same time:
|
||||
|
||||
.. code-block:: nim
|
||||
import threadpool, ...
|
||||
|
||||
# wait until 2 out of 3 servers received the update:
|
||||
proc main =
|
||||
var responses = newSeq[FlowVarBase](3)
|
||||
for i in 0..2:
|
||||
responses[i] = spawn tellServer(Update, "key", "value")
|
||||
var index = blockUntilAny(responses)
|
||||
assert index >= 0
|
||||
responses.del(index)
|
||||
discard blockUntilAny(responses)
|
||||
|
||||
Data flow variables ensure that no data races
|
||||
are possible. Due to technical limitations not every type ``T`` is possible in
|
||||
a data flow variable: ``T`` has to be of the type ``ref``, ``string``, ``seq``
|
||||
or of a type that doesn't contain a type that is garbage collected. This
|
||||
restriction is not hard to work-around in practice.
|
||||
|
||||
|
||||
|
||||
Parallel statement
|
||||
------------------
|
||||
|
||||
Example:
|
||||
|
||||
.. code-block:: nim
|
||||
:test: "nim c --threads:on $1"
|
||||
|
||||
# Compute PI in an inefficient way
|
||||
import strutils, math, threadpool
|
||||
{.experimental: "parallel".}
|
||||
|
||||
proc term(k: float): float = 4 * math.pow(-1, k) / (2*k + 1)
|
||||
|
||||
proc pi(n: int): float =
|
||||
var ch = newSeq[float](n+1)
|
||||
parallel:
|
||||
for k in 0..ch.high:
|
||||
ch[k] = spawn term(float(k))
|
||||
for k in 0..ch.high:
|
||||
result += ch[k]
|
||||
|
||||
echo formatFloat(pi(5000))
|
||||
|
||||
|
||||
The parallel statement is the preferred mechanism to introduce parallelism in a
|
||||
Nim program. A subset of the Nim language is valid within a ``parallel``
|
||||
section. This subset is checked during semantic analysis to be free of data
|
||||
races. A sophisticated `disjoint checker`:idx: ensures that no data races are
|
||||
possible even though shared memory is extensively supported!
|
||||
|
||||
The subset is in fact the full language with the following
|
||||
restrictions / changes:
|
||||
|
||||
* ``spawn`` within a ``parallel`` section has special semantics.
|
||||
* Every location of the form ``a[i]`` and ``a[i..j]`` and ``dest`` where
|
||||
``dest`` is part of the pattern ``dest = spawn f(...)`` has to be
|
||||
provably disjoint. This is called the *disjoint check*.
|
||||
* Every other complex location ``loc`` that is used in a spawned
|
||||
proc (``spawn f(loc)``) has to be immutable for the duration of
|
||||
the ``parallel`` section. This is called the *immutability check*. Currently
|
||||
it is not specified what exactly "complex location" means. We need to make
|
||||
this an optimization!
|
||||
* Every array access has to be provably within bounds. This is called
|
||||
the *bounds check*.
|
||||
* Slices are optimized so that no copy is performed. This optimization is not
|
||||
yet performed for ordinary slices outside of a ``parallel`` section.
|
||||
|
||||
|
||||
Guards and locks
|
||||
================
|
||||
|
||||
Apart from ``spawn`` and ``parallel`` Nim also provides all the 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`: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 allow
|
||||
potential deadlocks to be detected during semantic analysis.
|
||||
|
||||
|
||||
Guards and the locks section
|
||||
----------------------------
|
||||
|
||||
Protecting global variables
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Object fields and global variables can be annotated via a ``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: 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:
|
||||
|
||||
.. code-block:: nim
|
||||
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. Why these are essential is
|
||||
explained in the `lock levels <#guards-and-locks-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 detect
|
||||
potential deadlocks during semantic analysis. 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].}:
|
||||
...
|
||||
|
||||
|
||||
Here is how a typical multilock statement can be implemented in Nim. Note how
|
||||
the runtime check is required to ensure a global ordering for two locks ``a``
|
||||
and ``b`` of the same lock level:
|
||||
|
||||
.. code-block:: nim
|
||||
template multilock(a, b: ptr TLock; body: untyped) =
|
||||
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).
|
||||
|
||||
The ``locks`` pragma can also take the special value ``"unknown"``. This
|
||||
is useful in the context of dynamic method dispatching. In the following
|
||||
example, the compiler can infer a lock level of 0 for the ``base`` case.
|
||||
However, one of the overloaded methods calls a procvar which is
|
||||
potentially locking. Thus, the lock level of calling ``g.testMethod``
|
||||
cannot be inferred statically, leading to compiler warnings. By using
|
||||
``{.locks: "unknown".}``, the base method can be marked explicitly as
|
||||
having unknown lock level as well:
|
||||
|
||||
.. code-block:: nim
|
||||
type SomeBase* = ref object of RootObj
|
||||
type SomeDerived* = ref object of SomeBase
|
||||
memberProc*: proc ()
|
||||
|
||||
method testMethod(g: SomeBase) {.base, locks: "unknown".} = discard
|
||||
method testMethod(g: SomeDerived) =
|
||||
if g.memberProc != nil:
|
||||
g.memberProc()
|
||||
|
||||
|
||||
Taint mode
|
||||
==========
|
||||
|
||||
The Nim compiler and most parts of the standard library support
|
||||
a taint mode. Input strings are declared with the `TaintedString`:idx:
|
||||
string type declared in the ``system`` module.
|
||||
|
||||
If the taint mode is turned on (via the ``--taintMode:on`` command line
|
||||
option) it is a distinct string type which helps to detect input
|
||||
validation errors:
|
||||
|
||||
.. code-block:: nim
|
||||
echo "your name: "
|
||||
var name: TaintedString = stdin.readline
|
||||
# it is safe here to output the name without any input validation, so
|
||||
# we simply convert `name` to string to make the compiler happy:
|
||||
echo "hi, ", name.string
|
||||
|
||||
If the taint mode is turned off, ``TaintedString`` is simply an alias for
|
||||
``string``.
|
||||
|
||||
Reference in New Issue
Block a user