C#
concurrent programming
lock statement
async methods
threading

Lock and Async method in C

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

In C#, lock and async solve different concurrency problems. lock protects shared in-memory state from simultaneous access by multiple threads, while async keeps a thread free while an I/O-bound operation is waiting. They are both useful, but you should not treat them as interchangeable tools.

Use lock for Short Synchronous Critical Sections

lock is the right tool when multiple threads might update the same in-memory resource at the same time and the protected work can finish synchronously.

csharp
1using System;
2using System.Threading;
3
4public class Counter
5{
6    private readonly object _gate = new object();
7    private int _value;
8
9    public void Increment()
10    {
11        lock (_gate)
12        {
13            _value++;
14        }
15    }
16
17    public int Read()
18    {
19        lock (_gate)
20        {
21            return _value;
22        }
23    }
24}

The critical section should stay small. A long-running lock increases contention and can reduce throughput across the entire application.

Why await Does Not Belong Inside lock

You cannot await inside a lock block. The compiler rejects it because await may suspend the method and resume later, which would break the strict enter-and-exit semantics required by a monitor lock.

This code is invalid:

csharp
1public async Task SaveAsync()
2{
3    lock (_gate)
4    {
5        await File.WriteAllTextAsync("data.txt", "hello");
6    }
7}

Even if it compiled, it would be conceptually dangerous because the lock could be held across an asynchronous suspension point.

Use SemaphoreSlim for Async-Aware Mutual Exclusion

When you need to serialize access in an asynchronous method, SemaphoreSlim is the usual replacement because it has WaitAsync.

csharp
1using System.IO;
2using System.Threading;
3using System.Threading.Tasks;
4
5public class FileWriter
6{
7    private readonly SemaphoreSlim _mutex = new SemaphoreSlim(1, 1);
8
9    public async Task SaveAsync(string content)
10    {
11        await _mutex.WaitAsync();
12        try
13        {
14            await File.WriteAllTextAsync("data.txt", content);
15        }
16        finally
17        {
18            _mutex.Release();
19        }
20    }
21}

This pattern protects the shared operation while still working correctly with await.

Keep CPU Work and Async I/O Separate

A useful mental model is:

  • Use lock when you need exclusive access to fast in-memory state.
  • Use async when the method spends time waiting on I/O.
  • Use SemaphoreSlim when an async method still needs exclusive access.

Sometimes both appear in the same class, but they usually protect different parts of the workflow. For example, you might use lock to update a small in-memory cache and await for the database call that follows, as long as the asynchronous work happens outside the locked section.

Common Pitfalls

The most common mistake is trying to put await inside lock. That fails at compile time and usually points to a design issue: the synchronization primitive does not match the async workflow.

Another mistake is blocking on asynchronous code with .Result or .Wait(), especially on UI or request-handling threads. That can cause deadlocks and defeats the benefit of asynchronous execution.

Developers also sometimes lock on the wrong object, such as this or a public object reference. That lets outside code participate in the same lock unexpectedly. Use a private dedicated field instead.

Finally, avoid wrapping long network or file operations inside a synchronous lock if you can restructure the code. Hold exclusive access only for the minimum amount of time needed to protect shared state.

Summary

  • 'lock protects short synchronous critical sections on shared memory.'
  • 'await cannot be used inside a lock block.'
  • Use SemaphoreSlim.WaitAsync() when async code still needs mutual exclusion.
  • Do not block on async code with .Result or .Wait() unless you fully understand the consequences.
  • Keep critical sections small and lock on a private object, not on public references.

Course illustration
Course illustration

All Rights Reserved.