If you're working with ASP.NET or ASP.NET Core C# and you need to prevent (or restrict) the access and/or execution of certain methods from multiple threads, the best approach you can arguably use is a semaphore-based implementation using one of the various techniques provided by the framework's System.Threading namespace, such as the lock statement or the Semaphore and SemaphoreSlim classes. More specifically:
- The lock statement acquires the mutual-exclusion lock for a given object, executes a statement block, and then releases the lock: while a lock is held, the thread that holds the lock can again acquire and release the lock. Any other thread is blocked from acquiring the lock and waits until the lock is released.
- The Semaphore class represents a named (systemwide) or local semaphore: it is a thin wrapper around the Win32 semaphore object. Win32 semaphores are counting semaphores, which can be used to control access to a pool of resources.
- The SemaphoreSlim class represents a lightweight, fast semaphore that can be used for waiting within a single process when wait times are expected to be very short. SemaphoreSlim relies as much as possible on synchronization primitives provided by the common language runtime (CLR): however, it also provides lazily initialized, kernel-based wait handles as necessary to support waiting on multiple semaphores.
Here's a typical lock implementation example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
using System; using System.Threading; using System.Diagnostics; namespace LockSamples { class Program { static object lockObj = new object(); static void Main() { lock (lockObj) { Console.WriteLine("Another method"); } } } } |
In the above code, the static Main method content is locked and therefore accessible by a single thread at a time: all other threads will wait for the lock to be released before being allowed to access.
Here's a typical SemaphoreSlim usage example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
using System; using System.Threading; using System.Threading.Tasks; namespace LockSamples { class Program { static SemaphoreSlim semaphore; static void Main() { // create a semaphore that allows 1 maximum thread semaphore = new SemaphoreSlim(0, 1); // Enter semaphore using Wait() or WaitAsync() SemaphoreSlim.Wait(); // Perform the action(s) Console.WriteLine("Another method"); // Release the semaphore using Release() or ReleaseAsync() SemaphoreSlim.Release(); } } } |
As we can see, the SemaphoreSlim implementation is more versatile than the lock statement, as it can be easily configured to allow a controlled number of threads: in the above example we've initialized the class to only allow a single thread, but we could easily change it to 2, 3 or more: furthermore, since the SemaphoreSlim class supports the WaitAsync() method, such approach can be used to lock async statements as well.
What I found to be missing in both lock and SemaphoreSlim approaches was a method to conditionally lock the thread based upon an arbitrary unique ID, which could be very useful in a number of common web-related scenarios, such as whenever we want to lock multiple threads executed by the same user from entering a given statement block, without restricting that block to other users/threads.
For that very reason I've put togheter LockProvider, a simple class that can be used to selectively lock objects, resources or statement blocks according to given unique IDs.
Here's the source code, released under MIT license and fully available on GitHub:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
using System.Collections.Concurrent; using System.Threading; using System.Threading.Tasks; namespace Ryadel.Components.Threading { /// <summary> /// A LockProvider based upon the SemaphoreSlim class /// to selectively lock objects, resources or statement blocks /// according to given unique IDs in a sync or async way. /// </summary> public class LockProvider<T> { static readonly ConcurrentDictionary<T, SemaphoreSlim> lockDictionary = new ConcurrentDictionary<T, SemaphoreSlim>(); public LockProvider() { } /// <summary> /// Blocks the current thread (according to the given ID) /// until it can enter the LockProvider /// </summary> /// <param name="idToLock">the unique ID to perform the lock</param> public void Wait(T idToLock) { lockDictionary.GetOrAdd(idToLock, new SemaphoreSlim(1, 1)).Wait(); } /// <summary> /// Asynchronously puts thread to wait (according to the given ID) /// until it can enter the LockProvider /// </summary> /// <param name="idToLock">the unique ID to perform the lock</param> public async Task WaitAsync(T idToLock) { await lockDictionary.GetOrAdd(idToLock, new SemaphoreSlim(1, 1)).WaitAsync(); } /// <summary> /// Releases the lock (according to the given ID) /// </summary> /// <param name="idToUnlock">the unique ID to unlock</param> public void Release(T idToUnlock) { SemaphoreSlim semaphore; if (lockDictionary.TryGetValue(idToUnlock, out semaphore)) semaphore.Release(); } } } |
As we can see by looking at the code the class works with multiple SemaphoreSlim objects, hence can be used to perform synchronous or asynchronous locks.
Here's a sample usage that shows how the LockProvider class can be used to asynchronously lock a statement block:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
public static class SampleClass { private static LockProvider<int> LockProvider = new LockProvider<int>(); public static async Task DoSomething(currentUserId) { // lock for currentUserId only await LockProvider.WaitAsync(currentUserId); try { // access objects, execute statements, etc. result = await DoStuff(); } finally { // release the lock LockProvider.Release(currentUserId); } return result; } } |
To perform a synchronous lock, just use the Wait() method instead of WaitAsync().
Conclusions
That's it, at least for the time being: if you like the LockProvider class or want to share your feedbacks, feel free to use the comment section below or the GitHub issues page.