I see 4 common
ways to deal with states in a multi-threaded environment:
- Immutable objects: The idea is simple and efficient: make sure
that the states of objects cannot be changed. This way, the object can be
accessed concurrently by multiple threads without any risk of state corruption
or thread race. I wrote a blog post on how to implement immutability
with C# and how you can use NDepend
facilities to enforce object immutability and method purity.
- Thread / Resource affinity: The idea is as simple as it is
efficient here also: making sure that an object is always accessed by the same
thread. The .NET Framework offers several ways to implement this paradigm: the
System.ThreadStatic attribute on fields, Thread Local
Storage API and the interface ISynchronizeInvoke.
These are described in this article I published
on CodeProject.
- Synchronized objects / Thread-Safe class: Since v1.0, the .NET Framework comes
natively with a way to implement synchronized objects through the lock
C#/VB.NET keyword, alias the Monitor class. I found it convenient to implement
this pattern this way.
- Synchronization objects: The .NET Framework comes with
several synchronization API. Some of them are based on Windows synchronization
primitives (events, mutex, semaphore) and some of them are implemented by the
CLR (ReaderWriterLock). These API have been proven to be not optimal and even
buggy, with possible starvation phenomenon in some particular cases.
The cost of synchronizing
The goal of
this post is to praise immutability and affinity over synchronization. There
are 2 killer arguments against synchronization:
- First,
immutability and affinity are so efficient by-design
that once you go with them you won’t get anymore any concurrent access bug. Every experienced programmer knows that synchronizing accesses
to states is a real pain and generally ends-up in all sorts of
non-deterministic bugs very hard to pinpoint and correct.
- Second
immutability and affinity won’t hurt performance. Synchronizing thread accesses to states leads to thread-context-switches that
are extremely costly. Also, threads are very expensive resources that are
wasted each time a thread is put in a wait state. I agree that immutability
sometime provokes extra object creation but frankly, this is neglectable compare to
threading cost.
Why most of developers still
opt for synchronization?
First it is
a matter of education. Every programmer academic background comes with a course
on how to program in a multi-threaded environment with synchronization primitives.
This is an entertaining course illustrated with pleasent concurrent patterns and as
far as I know, tricks such as immutability and affinity are not presented.
Second, at
first glance immutability and affinity doesn’t seem to solve any problem. In both
case there are no shared and mutable states. And this is what programmers want:
being able to read and write a state from several threads. Just have a look at
this comment on my blog post on immutability:
I'm doing concurrency because I want to
calculate lots of things in parallel. I want them to change.
Immutable types don't help concurrency much. Sure you can prevent
accidental changes to things that shouldn't change. Of course that's just
as valuable when coding single threaded. I really don't think you can have
done much concurrent programming.
Immutable object / Changing state
Immutability
is more subtle than this. The string class is immutable for example. Did you
hear any programmer complain that he cannot code multi-threaded access to its
strings? No, and the non-obvious reason actually lies in how we consider the state’s
identity. A state is typically identified by an object and we write things
like:
Person person = new Person(“Julien”);
person.FirstName = “Mathieu”;
We also
write things like:
string personFirstName = “Julien”;
personFirstName = “Mathieu”;
The
difference is that in the first case we have non-thread safe code because the
object person can be read/write accessed by several threads. In the second case
the state personFirstName had also been modified but now we won’t get
multi-threading issue because the string class is immutable and the same state
is handled by 2 different strings. We are losing the common one-to-one relation
between a state and the underlying object. In other words, the identity of the
state is not the object anymore but the value itself. But still, the state has
been changed! The programmer is now free to expose the new “Mathieu” state to
other threads without any kind of synchronization infrastructure.
The beauty of thread /
resource affinity
Thread /
Resource affinity is also a concept that deserves more attention. It promotes
task-oriented programming. A task is thoroughly executed by the same thread,
from A to Z, and its states are not visible from other tasks. One famous application of affinity is the way win32/Windows Form works. For a given
form, all tasks are executed by the same thread, essentially
painting/refreshing/timer operations and input/mouse/keyboard event handlers.
The immense benefit that Windows Form users get is that we can be sure that the
mouse click event handler won’t be triggered while the windows is re-painted.
Thus all the states maintained behind our form don’t need to be synchronized.
As we know, the down-side effect is that doing computation from the UI
thread leads to unresponsive UI, and this is why there are tricks such as the
BackgroundWorker
class to make it easy to trigger task on tier threads.
To
implement affinity, personally I am not a fan of ThreadStatic or Thread Local
Storage because they both rely on costly Windows and CLR internals. Also, I
remember this article from Juval Lowy that explains how to implement
ISynchronizeInvoke
but I especially remember how complicated and error-prone it is. I prefer to
implement affinity by myself by:
Preferring
to maintain states with thread affinity from method local variables instead of
object fields. Local variables are by-design
bound to the current thread.
Writing
contracts about affinity. Concretely, all methods of a class begin with
something like:
Debug.Assert( MyHelperPlumbing.CurrentThreadIsInitialThread()
);
The code is
less readable but as long as the language teams don’t provide DbC facilities we
don’t have the choice.
We are
currently removing most of synchronized accesses from the code of NDepend. This
is a needed refactoring that will greatly simplify the development of features we want to do next. I admit that managing changing states through
immutable objects sometime require some intellectual gymnastic. For example, if
the object tagged by a DataGridViewRow is immutable, the tag property needs to
be updated when the state is changing. But clearly, this is a minor programming
exercise compare to synchronization nightmare.