I’d like to talk about .NET events and thread safety, but I first need to clarify what we mean by “thread safety.” After all, the most important consideration as regards thread safety is how thread-safe your code has to be. If you can get away with being thread-hostile, you should do so. There’s no sense in worrying about other threads in a single-threaded console application, for example.
If you are writing components, it is critically important that you document your level of thread-safety. Having a consistent vocabulary for describing thread safety thus becomes important.
Here is our preferred taxonomy for describing degrees of thread safety, adapted from this article, and from Joshua Bloch’s Effective Java Programming Language Guide.
Instances of this class appear constant to their clients; no external synchronization is necessary. Examples include String and Regex.
Most immutable objects need no internal synchronization; however, “lazy loading” will generally require some form of synchronization in order to maintain thread safety.
Instances of this class are mutable, but all methods contain sufficient internal synchronization that instances may be used concurrently without the need for external synchronization. Examples include many of the System.Threading types, including ManualResetEvent and Timer.
Since most concurrent applications require more coarsely-grained synchronization than the per-method-call synchronization of a thread-safe class, and since thread-safety causes unnecessary overhead for single-threaded operations, truly thread-safe classes are often not needed.
Like thread-safe, except that the class (or an associated class) contains methods that must be invoked in sequence without interference from other threads. To eliminate the possibility of interference, the client must obtain an appropriate lock for the duration of the sequence.
The object returned by Hashtable.Synchronized, for example, is documented as such: “Synchronized supports multiple writing threads, provided that no threads are reading the Hashtable. The synchronized wrapper does not provide thread-safe access in the case of one or more readers and one or more writers.”
Many classes are conditionally thread-safe if they are not being mutated.
Examples include List
Readers can only presume thread-safety on an instance of this type if they can be certain that there will be no writers on any thread. If there is any possibility of a writer, both readers and writers must use the same external synchronization.
It is easy to assume that a class is thread-safe for multiple readers, but unsynchronized “lazy loading” can easily cause a class to violate this degree of thread safety. Worse, a future “upgrade” of the class could introduce such a thing as an optimization. If a class is thread-safe for multiple readers, its documentation must indicate as such.
Some classes are mostly thread-compatible (see below), except for a few methods that are thread-safe. For example, the BeginInvoke method of Control can safely be called from any thread, even though Control otherwise has thread affinity.
It can also be convenient to declare a class thread-safe except for the Dispose method. See Thread-safe disposable objects for details.
A “freezable” object has a method (often named Freeze) that causes the object to become immutable and thus thread-safe. Until that point, the object is mutable and generally thread-compatible or even thread-hostile. Examples include classes that derive from the Freezable class of WPF, including Brush, Pen, and Transform.
Instances of this class can safely be used concurrently by surrounding each method invocation (and in some cases, each sequence of method invocations) by external synchronization. Most classes in the .NET Framework fall into this category and are documented as such: “Any public static members of this type are thread safe. Any instance members are not guaranteed to be thread safe.”
A thread-affined class has thread affinity; that is, instances of that class may only be used from one thread, generally the thread with which the instance was created. Most user interface components (Windows Forms, WPF, etc.) have affinity to the “user interface thread” and can thus only be used from that thread. Methods of thread-affined classes should assert that they are being called on the proper thread.
This class is not safe for concurrent use by multiple threads, even if all method invocations are surrounded by external synchronizaton. By this definition, thread-affined classes are also thread-hostile. In general, the only classes in the .NET Framework that are “thread hostile” are those with thread affinity. Thread-hostile behavior also includes:
Modifying static data that affect other threads is thread-hostile behavior, for example, setting the value of System.Console.OutputEncoding.
Posted by Ed Ball on March 28, 2008