읽기와 쓰기 충돌을 회피하는 비잠금 비동기 방법

읽기와 쓰기 충돌을 회피하는 비잠금 비동기 방법

이철우

같은 데이터에 대해 하나의 작업이 비동기 쓰기를 하고 몇 개의 작업이 비동기 읽기를 하는 경우에, 데이터 충돌을 회피하는 기본적인 방법은 lock 문[참고 4]를 활용하는 것이었다. .Net Core 1.0/.Net Framework 3.5/.Net Standard 1.0 부터는 클래스 ReaderWriterLockSlim[참고 2]를 소개했다. 또한 .NET 9 및 C# 13부터는 클래스 Lock[참고 3] 형식을 쓸 수 있다.

이 글에서는 ReaderWriterLockSlim 를 대체할 수 있는 비잠금 비동기 AsyncReaderWriterLock[참고 1] 을 소개한다. 이는 시스템 상태를 감시하기에 좋다. 아래 AsyncReaderWriterLock 시험 코드는 .Net 9 프로젝트이다. Nuget 에서 DotNext.Threading 를 찾아 설치한다.

아래 시험 코드에서 한 개의 작업이 10 ~ 19 msec 간격으로 쓰기를 하고, 다섯 개의 작업이 3 ~ 6 msec 간격으로 읽기를 할 것이다. 5 초 지나면 시험을 멈춘다. 시험을 확인하기 위하여 읽기와 쓰기 뒤에 데이터와 시각(Tick)을 출력하였다.

    public class TestAsyncReaderWriterLock
    {
        private AsyncReaderWriterLock? _asyncLock;
        private CancellationTokenSource? _cts;
        private bool _isRunning;
        private readonly Random _random = new();
        private int _data;

        public async Task Run()
        {
            if (_isRunning)
            {
                return;
            }

            using var asyncLock = new AsyncReaderWriterLock();
            using var cts = new CancellationTokenSource();
            _asyncLock = asyncLock;
            _cts = cts;

            _isRunning = true;
            _ = Write(cts.Token).ConfigureAwait(false);
            _ = Read("1", cts.Token).ConfigureAwait(false);
            _ = Read("2", cts.Token).ConfigureAwait(false);
            _ = Read("3", cts.Token).ConfigureAwait(false);
            _ = Read("4", cts.Token).ConfigureAwait(false);
            _ = Read("5", cts.Token).ConfigureAwait(false);
            while (!cts.IsCancellationRequested)
            {
                await Task.Delay(_random.Next(1, 10)).ConfigureAwait(false);
            }
            _isRunning = false;
        }

        public void Stop()
        {
            if (_isRunning)
            {
                _cts?.Cancel();
            }
        }

        private async Task Read(string id, CancellationToken token)
        {
            await Task.Run(async () =>
            {
                var data = 0;
                var tick = 0L;

                while (!token.IsCancellationRequested)
                {
                    await _asyncLock!.EnterReadLockAsync(token).ConfigureAwait(false);
                    try
                    {
                        data = _data;
                        tick = TimeProvider.System.GetLocalNow().Ticks;
                    }
                    finally
                    {
                        _asyncLock!.Release();
                        Console.WriteLine($"Read: {id} {data} {tick % 100000000L}.");
                        await Task.Delay(_random.Next(3, 7), token).ConfigureAwait(false);
                    }
                }
            }, token).ConfigureAwait(false);
        }

        private async Task Write(CancellationToken token)
        {
            await Task.Run(async () =>
            {
                var data = 0;
                var tick = 0L;

                while (!token.IsCancellationRequested)
                {
                    await _asyncLock!.EnterWriteLockAsync(token).ConfigureAwait(false);
                    try
                    {
                        data = _random.Next(10, 20);
                        _data = data;
                        tick = TimeProvider.System.GetLocalNow().Ticks;
                    }
                    finally
                    {
                        _asyncLock!.Release();
                        Console.WriteLine($"Write: {data} {tick % 100000000L}.");
                        await Task.Delay(data, token).ConfigureAwait(false);
                    }
                }
            }, token).ConfigureAwait(false);
        }
    }

위 시험 코드를 구동하는 코드가 아래 있다.

Console.WriteLine("Hello, World!");

var asyncLock = new TestAsyncReaderWriterLock();
_ = asyncLock.Run().ConfigureAwait(false);
await Task.Delay(5000).ConfigureAwait(false);
asyncLock.Stop();

Console.WriteLine("Bye.");

[참고 1] AsyncReaderWriterLock
[참고 2] ReaderWriterLockSlim
[참고 3] class Lock
[참고 4] lock statement

2개의 좋아요