놀라움: C# lock 문을 'Intercept' 할 수 있다
Source: Dev.to

C#에서 lock을 “가로채”(hijack) 할 수 있습니다. 작동 방식과 절대 이렇게 하면 안 되는 이유를 살펴보세요.
많은 개발자들은 lock이 일종의 런타임 “마법”이라고 생각합니다. 사실 그렇지 않습니다. 대부분의 경우 이는 단순한 구문 설탕(syntax sugar)이며, 컴파일러가 System.Threading.Monitor에 대한 호출로 변환합니다.
그 변환 과정에 보기 흉한 모서리 케이스가 있습니다. 프로젝트에서 자체 System.Threading.Monitor를 정의하면, 컴파일러가 BCL(Basic Class Library) 버전이 아니라 당신의 타입에 바인딩할 수 있습니다. 즉, lock이 의미하는 바를 바꿀 수 있다는 뜻입니다.
이는 파티 트릭이자, 동시에 위험한 함정(foot‑gun)입니다. 기법이라기보다 경고적인 이야기로 받아들여 주세요.
Note: Starting with .NET 9 and C# 13,
lockhas a special fast‑path when the expression is aSystem.Threading.Lock. In that case it compiles tousing (x.EnterScope()) { … }instead ofMonitor.Enter/Exit.
The hijack shown below applies to the classiclock (object)path.
lock에 대해 컴파일러가 생성하는 코드
고전적인 lock (obj) { … } 경우에 C# 컴파일러는 다음과 동등한 코드를 생성합니다:
object _lockObj = obj;
bool _lockWasTaken = false;
try
{
System.Threading.Monitor.Enter(_lockObj, ref _lockWasTaken);
// Your code...
}
finally
{
if (_lockWasTaken) System.Threading.Monitor.Exit(_lockObj);
}
따라서 lock은 런타임에서 “특별한” 것이 아닙니다. 알려진 호출로 확장되는 컴파일러 패턴일 뿐입니다.
하이재킹 트릭
BCL 타입과 동일한 완전한 이름을 가진 타입을 정의하면, 컴파일러가 당신의 메서드를 호출하도록 만들 수 있습니다.
최소 예제 (LINQPad 스니펫):
using System;
class Program
{
static readonly object _sync = new();
static void Main()
{
// 일반적인 lock처럼 보이지만, 실제로는 하이재킹되었습니다.
lock (_sync)
{
Console.WriteLine("Working inside the lock...");
}
}
}
// 우리는 System 네임스페이스와 클래스를 "가짜"로 만들었습니다.
namespace System.Threading
{
public static class Monitor
{
public static void Enter(object obj, ref bool lockTaken)
{
Console.WriteLine("Hijacked: Enter() was called");
lockTaken = true; // 중요: 그렇지 않으면 Exit()가 호출되지 않습니다.
}
public static void Exit(object obj)
{
Console.WriteLine("Hijacked: Exit() was called");
}
}
}
실행하면 Hijacked: 메시지가 표시됩니다. 이것이 컴파일러가 lock을 당신의 System.Threading.Monitor에 바인딩했다는 증거입니다.
왜 이렇게 작동하나요?
이름 해석은 컴파일 시간에 발생하기 때문입니다.
컴파일에는 System.Threading.Monitor 타입이 포함되어 있고, 참조된 어셈블리에도 동일한 타입이 존재합니다. 컴파일러는 두 개를 모두 보고 하나를 선택합니다. 만약 컴파일러가 여러분의 타입을 선택하면, 리라이트는 여전히 발생하지만 여러분의 메서드를 대상으로 하게 됩니다.
이것이 바로 컴파일러 경고가 여러분에게 알려주려는 정확한 내용입니다.
경고를 무시하지 마세요
이 코드는 CS0436을 발생시켜야 합니다. 이 경우 이것은 “잡음”이 아니라 바로 핵심입니다.
CS0436은 다음을 의미합니다:
- 컴파일에 포함된 타입과 가져온 타입 사이에 충돌이 있음
- 컴파일러는 컴파일에 포함된 타입을 사용함
사용자 정의 Enter 메서드가 실제로 잠금을 걸지 않는다면, 여러 스레드가 동시에 가정된 임계 구역 안에서 실행될 수 있습니다. 이는 불변 조건을 깨뜨리고 가장 심각한 종류의 버그를 초래합니다: 재현하기 어렵고 디버거 아래에서는 사라지는 드문 레이스 상황.
우연히 발생할 수도 있습니다:
- 헬퍼 클래스가
Monitor라는 이름을 가지고 잘못된 네임스페이스에 포함됨 - 의존성이 충돌하는 타입을 제공함
- 경고가 전역적으로 억제되어
CS0436이 놓침
System.Threading.Monitor와 관련된 CS0436을 한 번이라도 보면, 즉시 멈추고 조사하십시오.
더 안전한 목표: 락 경쟁 측정
관찰성이 목표이고 방해가 아니라면, 더 나은 접근법이 있습니다: lock을 전혀 건드리지 않고 경쟁을 측정합니다.
.NET은 모니터 경쟁에 대한 런타임 이벤트를 제공합니다:
ContentionStart_V2– event id 81ContentionStop_V1– event id 91 (includesDurationNs)- 키워드는
ContentionKeyword(0x4000)
이러한 이벤트는 EventListener를 통해 수신할 수 있습니다.
(LINQPad 스니펫):
using System;
using System.Diagnostics.Tracing;
using System.Threading;
sealed class LockMonitor : EventListener
{
public long TotalWaitTimeNs;
int _durationIndex = -1;
protected override void OnEventSourceCreated(EventSource source)
{
if (source.Name == "Microsoft-Windows-DotNETRuntime")
{
EnableEvents(source, EventLevel.Informational, (EventKeywords)0x4000);
}
}
protected override void OnEventWritten(EventWrittenEventArgs eventData)
{
// ContentionStop_V1
if (eventData.EventId != 91 || eventData.PayloadNames is null)
return;
if (_durationIndex < 0)
_durationIndex = eventData.PayloadNames.IndexOf("DurationNs");
if (_durationIndex < 0)
return;
// DurationNs is documented as a Double. Convert and keep nanoseconds as a long for easy accumulation.
var durationNs = (long)Convert.ToDouble(eventData.Payload![_durationIndex]!);
Interlocked.Add(ref TotalWaitTimeNs, durationNs);
}
}
위 스니펫은 원본에 맞게 의도적으로 축약되었습니다.
이 접근 방식으로 얻는 것
lock은 실제 잠금으로 유지됩니다.- 스레드가 대기하는 시간을 수집합니다.
- “좋은” 코드 경로와 “나쁜” 코드 경로를 비교하고 경쟁 상황을 명확히 볼 수 있습니다.
보다 자세한 보고서(잠금 객체별, 스택 트레이스, 타임라인)가 필요하면 dotnet‑trace, PerfView, 또는 ETW/EventPipe 기반 프로파일러와 같은 도구를 사용하세요. 위의 작은 EventListener는 좋은 최소 데모입니다.
요약
lock은 일반적으로System.Threading.Monitor.Enter/Exit으로 확장되는 컴파일러 설탕(sugar)입니다.- 형식 이름 충돌은 컴파일러가 생성하는 코드를 바꿀 수 있습니다. CS0436은 의미가 바뀌고 있음을 경고합니다.
lock을 가로채는 것은 깔끔한 데모이지만 실제 코드에서는 매우 나쁜 생각입니다.- 진단을 위해서는 트릭보다 경쟁 이벤트와 적절한 도구를 사용하는 것이 좋습니다.
