C# lock is mechanism to prevent concurrent access to “restricted” resource in multi-threaded environment. In multi-threaded applications locks are used to ensure that the current thread executes a block of code to completion without interruption by other threads. The lock statement obtains a mutual exclusion lock for a given object so that one thread executes the code block at a time and exits the code block after releasing the lock.
In C# apps, standard way to prevent concurrent access to the code is by using lock statement. Usage is relatively straightforward, but (as everything in “multi-threading”) usage can also be tricky, not to say dangerous.
While, lock statement works in “normal” code, async/await does not work with lock. I will use different approach here. Let’s take a look.
C# lock statement.
In C#, there is very simple way how to lock access to a resource. Most commonly, lock statement is used. Lets see an example.
1 2 3 4 5 6 7 8 |
private static void CountLock() { Console.WriteLine("Start counting sync with lock..."); lock (_locker) { StartCounting(); } } |
Where _locker
is declared as some instance of reference type, e.g.:
private static readonly object _locker = new object();
StartCounting()
is function which needs exclusive lock on some resource.
Simple example of lock.
In the next section, I will present simple example how to use lock and how application behaves if lock is used or if locking is not applied. Example below shows counter functions, one with locking and other without. I will show how these two behaves when used in parallel (multi-threaded environment).
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 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
using System; using System.Threading.Tasks; namespace Jenx.AsyncLocking { internal partial class Program { private static int _counter; private static readonly object _locker = new object(); private static void Main() { Console.WriteLine("**********************************************************"); Console.WriteLine("Starting counting in parallel 3 times to 10, NO sync lock."); Console.WriteLine("**********************************************************"); Parallel.For(0, 3, i => { CountNoLock(); }); Console.WriteLine("Hit any key to start locked version."); Console.ReadLine(); Console.WriteLine(); Console.WriteLine("************************************************************"); Console.WriteLine("Starting counting in parallel 3 times to 10, WITH sync lock."); Console.WriteLine("************************************************************"); Parallel.For(0, 3, i => { CountLock(); }); Console.WriteLine(); Console.WriteLine("*****************************************************"); Console.WriteLine("End test, hit key to exit."); Console.WriteLine("*****************************************************"); Console.ReadLine(); } private static void CountLock() { Console.WriteLine("Start counting sync with lock..."); lock (_locker) { StartCounting(); } } private static void CountNoLock() { Console.WriteLine("Start counting sync with no lock..."); StartCounting(); } private static void StartCounting() { _counter = 0; Console.WriteLine("Let's count..."); for (int i = 0; i < 10; i++) { _counter++; Console.WriteLine("Current counter is: " + _counter); } Console.WriteLine("Finished..."); } } } |
With no locking applied, I get:
With lock:
You can spot the difference, second example with lock does not allow “concurrent” access to locked resources, so output is as expected – after first counting is finished, second is started, etc… _counter
variable is used only in current context, no conflicts and unpredicted results.
What about async/await locking?
Until now everything was fine, just classical lock mechanism. But what about locking of async/await methods?
Just to summarize, async/await was introduced in C# 5 as part of Task-based Asynchronous Pattern (TAP). Noways, TAP is the recommended asynchronous design pattern for new development, therefore It’s widely used.
Let’s investigate locks in async/await. Logical approach would be to use similar approach as above, e.g.:
1 2 3 4 5 6 7 8 |
private static async Task CountAsyncWithLock() { Console.WriteLine("Start counting async with lock..."); lock (_locker) { await StartCountingSync(); } } |
but, this does not work. Compiler has problems with this, I get:
Cannot await in the body of lock statement
C# compiler
So, what now?
Let’s introduce SemaphoreSlim
! SemaphoreSlim
represents a lightweight alternative to Semaphore that limits the number of threads that can access a resource or pool of resources concurrently. In this case this objects handles locking.
Pattern of locking of async/await methods with SemaphoreSlim
is:
1 2 3 4 5 6 7 8 |
private static async Task CountAsyncWithLock() { Console.WriteLine("Start counting async with lock..."); await _semaphoreSlim.WaitAsync(); await StartCountingSync(); _semaphoreSlim.Release(); } |
where _semaphoreSlim
is defined and initialized as:
1 |
private static readonly SemaphoreSlim _semaphoreSlim = new SemaphoreSlim(1, 1); |
Just a note here: always use SemaphoreSlim.WaitAsync()
and SemaphoreSlim.Release()
with try-catch-finally
statement.
Simple example of async/await lock.
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 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 |
using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; namespace Jenx.AsyncLocking { internal class Program { private static int _counter; private static readonly SemaphoreSlim _semaphoreSlim = new SemaphoreSlim(1, 1); private static async Task Main() { Console.WriteLine("***********************************************************"); Console.WriteLine("Starting counting in parallel 3 times to 10, NO async lock."); Console.WriteLine("***********************************************************"); var executingTasks = new List<Task>(); for (int i = 0; i < 3; i++) { var executingTask = CountAsyncNoLock(); executingTasks.Add(executingTask); await Task.Delay(2000); // wait a bit for next task to start -> better see counter reset } Task.WaitAll(executingTasks.ToArray()); Console.WriteLine("Hit any key to start locked version."); Console.ReadLine(); Console.WriteLine(); Console.WriteLine("*************************************************************"); Console.WriteLine("Starting counting in parallel 3 times to 10, WITH async lock."); Console.WriteLine("*************************************************************"); executingTasks.Clear(); for (int i = 0; i < 3; i++) { var executingTask = CountAsyncWithLock(); executingTasks.Add(executingTask); await Task.Delay(2000); // wait a bit for next task to start -> better see counter reset } Task.WaitAll(executingTasks.ToArray()); Console.WriteLine(); Console.WriteLine("*****************************************************"); Console.WriteLine("End test, hit key to exit."); Console.WriteLine("*****************************************************"); Console.ReadLine(); } private static async Task CountAsyncNoLock() { Console.WriteLine("Start counting async no lock..."); await StartCountingSync(); } private static async Task CountAsyncWithLock() { Console.WriteLine("Start counting async with lock..."); await _semaphoreSlim.WaitAsync(); await StartCountingSync(); _semaphoreSlim.Release(); } private static async Task StartCountingSync() { _counter = 0; Console.WriteLine("Let's count async..."); for (int i = 0; i < 10; i++) { await Task.Delay(500); // faking doing something else async _counter++; Console.WriteLine("Current counter is: " + _counter); } Console.WriteLine("Finished..."); } } } |
So, no-lock version of async/await method returns this:
With SemaphoreSlim locker, output looks like this:
From upper outputs we can clearly see effect of locking in async/await methods in multi-threading environments.
Conclusion.
C# lock statement is used for mutual-exclusion lock for a given object, executes a statement block, and then releases the lock.
For locking TAP based methods, SemaphoreSlim is one way to do mutual-exclusion locking.
In this blog post, I quickly presented how both mechanisms can be used on simple examples.
One thought on “C# locks and async Tasks.”
SemaphoreSlim is just what I was looking for – thanks!