I’m writing a .NET library which exposes certain public APIs. Currently, I have not enforced thread safety in my library for following reasons apparent to me:
lock
s (Monitor.Enter
andMonitor.Exit
) are only limited to the AppDomain, so they do not provide synchronization in the inter-process situations.Mutex
can be used bigger scope more than the AppDomain, but then I need to struggle with OS-specific limitations such as naming rules for named mutexes.- Both
lock
andMutex
can be useless and cause performance costs if the consumer never wanted to use synchronization. - Consumers might need different synchronization mechanism such as
Semaphore
. - MSDN’s Managed threading best practices for class libraries states that avoid synchronization and not make instance data thread safe by default.
From these what I understood was, when it comes to class libraries, it is the consumers’ responsibility to enforce the thread safety in their own way when using that library and the developer should not worry about it.
Is my understanding correct? especially when writing any class library? Also, what would your approach when documenting the library? Should we explicitly state the absence of thread-safety or let the consumers assume that it is not thread safe?
6
You should make your library thread-safe, but that does not mean that you should be sprinkling synchronization primitives around your code base.
For the average library, which is not explicitly designed to communicate across threads, they should be thread-safe to the level that different threads can invoke methods on different objects (possibly of the same type) without interfering with each other or getting incorrect results. This is also known as thread-compatibility.
This can be done in several ways, in order of preference:
- Avoid using (writable) shared internal state that might be accessed from multiple threads. If a user decides to create an object and share that across threads, then it is the responsibility of the user to ensure proper synchronization.
- If it makes sense, put the internal data that gets shared between objects in thread-local storage.
- Put synchronization primitives around the access to the shared internal state.
If your library is specifically designed for inter-thread communication, then you quite quickly end up in the last option mentioned above.
6
Also, what would your approach when documenting the library? Should we explicitly state the absence of thread-safety or let the consumers assume that it is not thread safe?
It might be useful to take a look at the documentation of some common classes, for example List
Thread Safety
Public static (Shared in Visual Basic) members of this type are thread safe. Any instance members are not guaranteed to be thread safe.
It is safe to perform multiple read operations on a List, but issues can occur if the collection is modified while it’s being read. To ensure thread safety, lock the collection during a read or write operation. To enable a collection to be accessed by multiple threads for reading and writing, you must implement your own synchronization. For collections with built-in synchronization, see the classes in the System.Collections.Concurrent namespace. For an inherently thread-safe alternative, see the ImmutableList class.
This forms the basis of my expectations:
- Static members should be thread safe
- Objects should not have any special thread affinity. I.e. you can create an object on one thread, and use it from another.
- No hidden, unsynchronized, shared state. If shared state is needed, make this explicit in some way, or make sure access is synchronized. I.e. locking the object is sufficient for thread safety.
- If the class is named “Concurrent”, “Threadsafe”, or “Immutable”, it should be thread safe.
It is especially important to note any deviations from expectations. But I would recommend to be explicit in the documentation. There is few things worse than having to guess if something is safe or not, and some things might not be obvious, like if a method modifies internal state or not.
UI related objects are one notable exception from these general rules.
I would suggest making a general statement of the threading model used in your library, link to this from any type documentation to make it easier to find, and note any deviations.
It is your responsibility to make the library useable and useful for the consumer. It doesn’t have to be thread safe, but you need make that very clear in the documentation and explain what the expected usage pattern should be
A common way libraries that are not thread safe for multiple threads accessing a single object is to handle this is by building the library in a way that instantiating a new object per thread is safe (i.e. doesn’t share configurations via static variables or disk). In most cases the consumer can trivially handle this by changing to a transient scope on their IoC. The key part is letting the consumer know they need to do this.
It depends
I am contributing to a library that is thread safe by default. We decided to do this because making it thread safe by default makes our developer lives easier, reduces client misuse, there is no inherent performance gain by not being thread safe, and making it thread safe from the caller side is much harder while being somewhat trivial on our side. It also doesn’t require complicated things like system wide mutex.
If you do go down the thread safe path, be careful to avoid lock statements, as these may cause unexpected behaviors in async methods.
Regarding the MSDN guideline: Microsoft themselves distribute thread safe classes e.g: https://learn.microsoft.com/en-us/dotnet/api/system.collections.concurrent?view=net-8.0
By thread safe, this means:
- No shared state between different instances of the same class.
- Accessing the same object from different threads will not make the object’s internal state to become corrupt. It may result in unusual behavior, but not result in a crash or otherwise unrecoverable situation.
- Threads reading a property of the objects will get the latest written value of that property, when reading and writing occur on different threads. If a 2nd write occurs right after the read, then the read will not see the modified data.
- If a particular operation needs to happen on a specific thread (most commonly on the UI thread), the object will automatically handle that situation, even if the call comes from the wrong thread.
However, this only applies to the specifics of this library. My suggestion is to try and go thread safe if your library is likely to be used in a multithreaded environment, at the very least try to prevent corrupted states due to thread races. This will at the minimum save you time trying to educate people on how to avoid crashes caused by multithreading.
5
“Enforce thread safety” is difficult depending on what you are doing.
For example, if two threads read the same data, that should not crash, and give the same data to both threads.
What if one thread tries to read data while the other tries to change it, as close together as possible? You can’t guarantee that the data read is the old of the new data. You can guarantee that it doesn’t crash and is either the old or the new data. But either way it’s a problem. So do you want to make any guarantees?
It’s hard to decide what guarantees you should give.