Threads and mutable data
We have just seen how channels can be used to pass data between threads. This is the preferred means of thread communication in Fuzion. However, there are situations where mutable shared memory is better suited. This cannot be done in Fuzion using explicit synchronization, which will be explained in this section.
Defining a shared memory data structure
Fuzion does not permit direct mutation of fields or array elements. If it is
desired to change data, this can only be done using a mutate
effect.
To illustrate this, we define a simple counter feature and use
it to count the number of integers divisible by 7 in a given
interval:
Single threaded case
The previous example defines my_mutate as a child
of mutate, which is the Fuzion base library's standard effect for
mutation. This effect does not permit being used by concurrent threads, since
it does not ensure exclusive access via locking or similar mechanisms.
Multiple threads using mutate
Now we might want to speed this code up by running 10 parallel threads as follows:
However, this results in an error since mutate permits to be used
by a single thread only since it does not do anything to ensure consistency of
shared data. We need a more powerful version of mutate in this
case.
blocking_mutate
For multi-threaded use, the concur.blocking_mutate effect has
been added, so we must use this for out example using threads:
But, unfortunately, this still results in an error since we now have racy
accesses to field counter.val. We have to make sure that these
accesses are exclusive by the respective threads. This is done using a call
to the mutate effect's exclusive operation.
For our small counter example, this means that we must make the
code val <- val+1 exclusive since otherwise concurrent
threads T1 and T2 may read the same value, e.g., 42
from val, and both write the incremented value 43
back. The situation could even be worse: If T1 was preempted by some
unrelated thread T3 just after reading 42 such
that T2 can perform many calls to increment
before T3 yields the CPU back to T1, which will then write a
fully outdated 43 to the counter, undoing all the increments
performed by T2 meanwhile.
Wrapping the code in counter.increment
with M.env.exlusive makes sure at any time, only one thread may
read and modify val, avoiding the described inconsistencies. The
disadvantage is that a thread may get blocked waiting for another thread to
finish execution of the exclusive code section.
Note that feature counter.read also requires synchronization, but
for a more subtle reason: Memory accesses might be re-ordered at different
levels (within the CPU, the processor cache hierarchy, or even in the code
generated by the compiler) to increase performance. This is permitted as long
as there is no visible change in single-thread behavior. Here, this means
that read may return an outdated value if changes to the counter
were made by other threads only. Making read exclusive
introduces fences that prohibit the re-use of such outdated values.
So, here we finally have a working multi-threaded version of our example:
Sharing code for single- and multi-threaded use
The single-threaded mutate that was used in the original,
single-threaded example provides features like exclusive as well,
even though, as we have seen, multi-threaded accesses are not permitted. The
implementation just provides essentially empty implementations
of exclusive and related features. This permits the use of data
structures that were developed for multi-thread uses with
proper exclusive sections to be used in a single-thread
using mutate without invoking the synchronization overhead
that blocking_mutate entails.