While you only occasionally need to make a decision about whether or not to use multiple threads, every time you design a type you need to consider what its thread safety model is going to be. This will very much depend on its intended usage. Some types naturally end up being used only within a single thread, others naturally end up being shared across threads, and some are used specifically for thread handling, and often have their own special characteristics. One of the most important things to do is to document the type's behaviour: which methods are thread-safe, which aren't, etc. Below are some common models. Note that when I talk about methods, I'm including properties as well.
This is the most commonly found model in the framework. Basically, any static methods can be called by any thread at any time with no nasty side-effects. (There may be side-effects, but they shouldn't cause anything to get into an invalid state.) Instance methods, however, should only be called on a single thread at a time, and there should really be a memory barrier (in the calling code) before using an instance that has previous been used by another thread, and another memory barrier (again, in the calling code) after changing an instance that is thereafter going to be used by another thread. In practice, this is probably one of the areas most people are sloppiest in, just passing objects from one thread to another without making sure things are synchronized specifically - and in practice, it's usually going to be fine, because part of "handing over" an object usually involves a memory barrier in both threads anyway. None of that is the responsibility of the type in question with this model, however - instance methods should basically assuming they're running in a single thread, and not worry about locking in order to query/update values. Of course, if you know that one of the members of the type may be shared amongst threads, using that member may involve locking, depending on the thread-safety of the type of that member.
If a type has a thread affinity, that usually means you can only use it
(or most of it) from the thread it was created on. Controls are the most
obvious examples of this - in the Windows Forms
section I've already talked about the way that you're not meant to use
any methods other than Invoke
, BeginInvoke
,
EndInvoke
, InvokeRequired
and CreateGraphics
on a control unless you're running in the thread responsible for it.
Designing your own type like this should be a relatively rare occurrence,
unless it's to do with the GUI, or uses something else with thread affinity.
Some methods and properties naturally use thread-local storage - that is
to say, each thread has its own individual value for a variable.
Thread.CurrentPrincipal
is a good example of this - compare
it with Thread.CurrentCulture
, which is an instance property.
Most methods and properties using thread-local storage are static, just like
Thread.CurrentPrincipal
- having one value per instance and
per thread rarely makes sense, and indeed the simplest way of using
thread-local storage is with ThreadStaticAttribute
which can
only be applied to static fields (which can then be returned by static
properties, etc).
Sometimes, it's important for a type to be totally thread-safe, so that anyone
can use any instance of it from any thread, with no internal inconsistencies or
unwanted side-effects. This is particularly true of types which are typically
accessed using the factory or singleton pattern. Encoding
is a good
example of this - it would be a pain if threads couldn't use Encoding.ASCII
(or any other encoding) without carefully locking things.
Often, types like this are immutable - once they've been created, they don't change, so even without locking, it doesn't matter what you do with them. Assuming instances are acquired in a thread-safe way to start with (which usually involves a lock or a type initializer to ensure safety), immutable types are naturally thread-safe.
Some types can be accessed in some ways by multiple threads, but not in others.
Many collections fall into this category. For instance, from the documentation for
ArrayList
:
An ArrayList can support multiple readers concurrently, as long as the collection is not modified. To guarantee the thread safety of the ArrayList, all operations must be done through the wrapper returned by the Synchronized method. |
So, you can populate an instance in one thread, and thereafter read the contents from multiple threads. The documentation doesn't specify anything about handing over an instance from one thread to another (such as whether the client code has to make sure there's a memory barrier after the last write and another before the first read in another thread), which is unfortunate, but probably means you can't rely on it. This kind of case is where clear documentation is absolutely crucial. If a type can support multiple threads doing one thing and another thread doing something else (multiple reader, single writer tending to be the most common form) then the documentation should state exactly what is entailed - is that level of safety available without any extra locking on the part of the client, what are the guarantees about how soon that any "new" data is seen by other threads, etc.
Since originally writing this page, I've discovered Joe Duffy's blog, which includes a fabulous entry about when to make things thread-safe, when to lock etc. I can't recommend this enough.
Back to the main C# page.