Writing public libraries: Should I let the consumer of the library enforce thread safety


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:

  • locks (Monitor.Enter and Monitor.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 and Mutex 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?

Best Answer

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:

  1. 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.
  2. If it makes sense, put the internal data that gets shared between objects in thread-local storage.
  3. 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.