C# - async 메서드에서의 System.Threading.Lock 잠금 처리
.NET 9부터 새롭게 도입한 System.Threading.Lock 타입 역시 Monitor 및 Mutex와 그대로 치환이 됩니다. 즉, lock을 소유한 스레드를 알고 있으며 재진입에 따른 잠금 횟수를 관리합니다.
따라서, 다음의 코드는,
internal class Program
{
static Lock _obj = new();
static void Main(string[] args)
{
_obj.Enter();
Thread t = new Thread(() =>
{
_obj.Exit(); // 실행 시: System.Threading.SynchronizationLockException: 'The calling thread does not hold the lock.'
});
t.Start();
t.Join();
}
}
실행 시점에 Exit를 호출하는 코드에서 예외가 발생합니다. 마찬가지로 다음의 코드도,
internal class Program
{
static Lock _obj = new();
static void Main(string[] args)
{
_obj.Enter();
_obj.Enter(); // 2번 잠금
Thread t = new Thread(() =>
{
_obj.Enter(); // 다른 스레드에서 lock을 얻으려고 시도
Console.WriteLine("Thread 1");
_obj.Exit();
});
t.Start();
_obj.Exit(); // 한 번 잠금을 해제
Thread.Sleep(5000);
_obj.Exit(); // 두 번 잠금을 해제 - 이 시점에 "Thread 1"이 출력됨
t.Join();
}
}
Main 스레드에서 _obj.Exit를 2번 호출한 시점에야 lock이 풀려 Thread의 내부 코드가 실행됩니다.
비동기 문맥에서도 역시 Monitor와 동일한 동작을 보이는데, lock 예약어를 이용한 코드는 아예 컴파일 단계에서 오류를 발생시키고,
internal class Program
{
static Lock _obj = new();
static async Task Main(string[] args)
{
lock (_obj)
{
await Task.Delay(2000); // error CS4007: Instance of type 'System.Threading.Lock.Scope' cannot be preserved across 'await' or 'yield' boundary.
}
}
}
Enter/Exit로 풀어내면 컴파일 오류는 피할 수 있지만, Exit 시점에 여전히 예외는 발생합니다.
internal class Program
{
static Lock _obj = new();
static async Task Main(string[] args)
{
_obj.Enter();
await Task.Delay(2000);
// 컴파일은 되지만,
_obj.Exit(); // 실행 시: System.Threading.SynchronizationLockException: 'The calling thread does not hold the lock.'
}
}
SynchronizationContext 환경을 제공하는 Windows Forms/WPF까지 Enter/Exit로 풀어내 사용하는 것은 Monitor와 일치합니다.
Lock _obj = new();
private async void Form1_Load(object sender, EventArgs e)
{
_obj.Enter();
try
{
await Task.Delay(2000);
}
finally
{
_obj.Exit(); // SynchronizationContext에 따라 Enter를 호출한 스레드에서 Exit를 호출하므로 동기화 성공
}
}
유의해야 할 점은, (lock 예약어가 내부적으로 사용하는) Scope를 사용한 경우에는 컴파일 오류가 발생한다는 점입니다.
var scope = _obj.EnterScope();
try
{
await Task.Delay(2000);
}
finally
{
scope.Dispose(); // 컴파일 오류: error CS4007: Instance of type 'System.Threading.Lock.Scope' cannot be preserved across 'await' or 'yield' boundary.
}
따라서 SynchronizationContext가 있음을 알고 의도적으로 System.Threading.Lock을 사용하는 경우라면 Enter/Exit를 사용해야 합니다. (참고로, scope.Dispose 코드를 주석 처리하면 컴파일은 되지만, 당연히 lock은 해제되지 않아 결국엔 문제가 발생합니다.)
정리하면, System.Threading.Lock 역시 (스레드가 바뀌는) 비동기 문맥에서는 사용할 수 없고,
그런 경우를 원한다면 Semaphore(Slim)을 사용해야 합니다.
(
첨부 파일은 이 글의 예제 코드를 포함합니다.)
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]