C# - async-await에서 lock 사용하기

출처

C#에서 비동기 메소드에서는 lock을 쓸 수 없다. 이 글은 그래도 lock을 사용 하고 싶을 때를 위한 것이다.

lock이 필요한 경우를 예를 들면 아래처럼 더블 체크 락킹을 하고 싶을 때이다.

// /이것이 여러 스레드에서 비동기에게 불리는
private static async ValueTask 조건을만족하면무엇인가하는Async()
{
    if (조건)
    {
        lock (_lockObj) 
        {
            if (조건)
            {
                await 꽤무거운IO작업Async();
            }
        }
    }
}

lock 스테이트먼트는 비동기 메서드 내에서 사용할 수 없기 때문에, 실제로는 이 코드는 컴파일이 되지 않는다.

위 문제를 간단하게 처리할 수 있는 방법 중 하나로 세마포어를 사용한다.
보통 세마포어는 프로세스 간 동기화에 사용하지만 .NET에는 프로세스 내에서 이용하기 위한 SemaphoreSlim 클래스가 있다.

SemaphoreSlim 클래스에는 비동기로 기다릴 수 있는 WaitAsync() 메소드가 있다.
이것을 사용하면 완전히 비동기에서 lock을 사용할 수 있다.

범용적으로 사용할 수 있게 AsyncLock 이라는 이름으로 클래스를 만들어두면 좋을 것이다.

/// async인 맥락에서의 lock을 제공한다.  
/// Lock 해제를 위해 반드시 처리 완료 후에 LockAsync가 생성한 IDisposable을 Dispose 한다. 
public sealed class AsyncLock
{
    private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);

    public async Task<IDisposable> LockAsync()
    {
        await _semaphore.WaitAsync();
        return new Handler(_semaphore);
    }

    private sealed class Handler : IDisposable
    {
        private readonly SemaphoreSlim _semaphore;
        private bool _disposed = false;

        public Handler(SemaphoreSlim semaphore)
        {
            _semaphore = semaphore;
        }

        public void Dispose()
        {
            if (!_disposed)
            {
                _semaphore.Release();
                _disposed = true;
            }
        }
    }
}

사용할 때는, lock 구문 대신 using 구문을 사용한다. 이것에 의해서, semaphore 관리를 잊고 lock 이라는 의미를 부여한 코드를 사용한다.

예를 들어, 처음의 예제 코드에서는 아래처럼 사용할 수 있다.

private static readonly s_lock = new AsyncLock();
private static async Task 조건을만족하면무엇인가하는Async()
{
    if (조건)
    {
        using (await s_lock.LockAsync()) 
        {
            if (조건)
            {
                await 꽤무거운IO작업Async();
            }
        }
    }
}

이 글은 2020-03-31에 작성되었습니다.