NiceThreads is threading utility library designed to make different threading primitives easier to use with a more consistent API. It started out of frustration with the different options (and more specifically, the different APIs) for enabling thread safety and locks in the .NET framework and how much code was required to use some of them. NiceThreads provides a consistent interface for standard Monitor
locks and the ReaderWriterLockSlim
class (and possibly others in the future). It also provides support for activating and deactivating these locking primitives through the disposable pattern. Finally, it provides wrappers that can easily provide thread-safety to unsafe objects.
Common API
Both Monitor
(and the "lock" statement which is syntactic sugar for Monitor
) and ReaderWriterLockSlim
attempt to solve the same problem: preventing conflicting concurrent access to objects that might need to be read or written to by multiple threads. They both do this by limiting access to the object to one thread at a time (or in the case of read locks provided by ReaderWriterLockSlim
, only to threads that signal they want read-only access) while making other threads wait their turn. However, even though both classes provide similar functionality they are intended for different uses and have different tradeoffs. Further, they use similar but different APIs making switching between them difficult.
To solve this problem, NiceThreads provides a consistent ILocker interface that has implementations wrapping both classes and provides a consistent API. The rest of NiceThreads is designed to interact with ILocker
allowing interchangeable use of the different types of locking primitives. In addition, the ILocker
interface can be used directly to provide a consistent wrapper around either locking class for your own code.
For example:
ILocker locker = new ReaderWriterLockSlimLocker(); locker.EnterReadLock(); // Do work... locker.ExitReadLock(); locker = new MonitorLocker(); locker.EnterReadLock(); // Do work... locker.ExitReadLock();
Disposable Pattern
Both locking primitives require explicitly activating the lock and subsequently manually removing the lock when finished. This can lead to problems if the developer forgets to release the lock or ends up exiting the normal program flow (for example, because an exception was thrown). The lock
keyword in C# attempts to make this design easier to use for the Monitor
class by abstracting Monitor
instantiation and surrounding it's use in a control block, however, no such keyword exists for other locking classes such as ReaderWriterLockSlim
. In addition, using the lock
keyword means some control is lost over the lifecycle and usage of the underlying Monitor
class.
NiceThreads attempts to solve this problem by providing a set of classes that implement IDisposable
and wraps an underlying ILocker
(which in turn provides consistent access to alternate framework locking classes). They activate the requested lock type on instantiation and free it on disposal. This allows the developer to use the built-in support for the disposable pattern in .NET to automatically free a lock when finished with it by using the using
statement.
For example:
ILocker locker = new ReaderWriterLockSlimLocker(); using(new ReadLock(locker)) { // Do work... }
Thread-Safe Wrappers
Even with the added convenience of a consistent API and disposable pattern support, implementing thread-safety for non-thread-safe objects can still require a fair amount of code. For every object that needs to be protected, a new locking object potentially needs to be created and maintained. NiceThreads helps implement thread safety for objects by providing wrapper classes that encapsulate generic locking logic and provide thread-safe access to their underlying object. SyncObject<T>
wraps an arbitrary type and ReadOnlySyncObject<T>
wraps an arbitrary type while providing "readonly" semantics (I.e., once the ReadOnlySyncObject<T>
has been constructed, it's underlying object cannot be changed). These classes provide a variety of methods to expose their wrapped object in thread-safe ways including thread-safe getting and setting, disposable pattern access, and action/function providers (I.e., lambdas or anonymous methods).
For example:
SyncObject<int> num = new SyncObject<int>(10); num.Sync = 20; // Access as a property with a thread-safe setter int value = num.Sync; // Access as a property with a thread-safe getter using(num.WriteLock()) { // We can now access using unsafe code num.UnsyncField++; // Provides direct field access value = num.Unsync; // Access as a property with an unsafe getter } num.DoWrite(n => n + 10); // Thread-safe write with an Action value = num.DoRead(n => n + 100); // Thread-safe read with a Func
Obtaining
NiceThreads is open source and released under the Apache 2.0 license. It can be obtained here: https://github.com/daveaglick/NiceThreads