Skip to main content

Parallel programming

Miking offers a set of externals for shared-memory parallelism using atomic operations and threads running on multiple cores.

The parallel programming primitives consist of atomic references and functions for creating and synchronizing threads. In addition to the examples below, more documentation can be found in the standard library at stdlib/multicore.

Atomic References

Atomic references are similar to ordinary references, except that operations performed on them are atomic, which means that no other execution thread can interfere with the result. In other words, they are safe to use in multi-threaded execution. Atomic references are provided in multicore/atomic.mc.

atomicMake creates a new atomic reference and gives it an initial value. The value of the atomic reference can be read by atomicGet:

include "multicore/atomic.mc"
mexpr
let a = atomicMake 0 in
utest atomicGet a with 0 in

atomicCAS a oldVal newVal performs an atomic compare-and-set, that is, it only updates the value of a to newVal if the current value is identical to oldVal, and then returns a Boolean representing if the update was successful or not:

utest atomicCAS a 0 1 with true in
utest atomicCAS a 42 3 with false in
utest atomicGet a with 1 in

The compare-and-set operation is currently supported for integer atomic references only.

To unconditionally set the value of an atomic reference, we can use atomicExchange, which also returns the old value of the reference:

utest atomicExchange a 2 with 1 in

Finally, for integer references, we can use atomicFetchAndAdd to increase or decrease the value of the reference. The function returns the old value of the reference:

utest atomicFetchAndAdd a 1 with 2 in
-- Current value is now 3
utest atomicFetchAndAdd a (subi 0 45) with 3 in
-- Current value is now -42

Multi-Threaded Execution

Functions for handling threads are provided in multicore/threads.mc. The following example program spawns 10 threads that compete for printing their IDs:

include "string.mc"
include "multicore/thread.mc"
mexpr
let place = atomicMake 1 in
let threads = create 10 (lam. threadSpawn (lam.
print (join
[int2string (atomicFetchAndAdd place 1)
, ": thread ID "
, int2string (threadSelf ())
, "\n"
]))
) in
map threadJoin threads

where threadSpawn takes a function of type () -> a as argument and threadSelf returns the ID of the current thread. Note that threadJoin must be called once for each call to threadSpawn. The output of the above program might be:

1: thread ID 1
2: thread ID 2
3: thread ID 129
4: thread ID 130
5: thread ID 3
6: thread ID 257
7: thread ID 258
8: thread ID 131
9: thread ID 385
10: thread ID 386

However, the values and order of the thread IDs might be different over different runs.

Synchronization

Externals for thread synchronization in the form of mutual exclusion locks and condition variables are defined in multicore/mutex.mc and multicore/cond.mc, respectively.

Probability distributions

Externals for probability distributions are defined in stdlib/ext/dist-ext.mc. To use these, you must install the opam package owl (i.e., opam install owl)