· 3 min read

C# lock의 내부 구현

C# lock 문이 내부적으로 Monitor.Enter/Exit를 호출하는 과정과, CLR의 Thin Lock 최적화, 스핀 대기 전략을 분석한다.

C# 프로그래밍을 하다보면 자연스레 멀티쓰레딩 환경에서 자원 획득 제한이 필요한 경우 lock을 사용했었다.

이 lock 키워드 및 블럭을 사용했을 때 내부적으로 어떻게 동작되는지 궁금해서 알아보았다.

lock 문

lock statement(lock 문) Microsoft Docs

lock은 공식 문서에 이라고 한다.

  • Language Reference
    • Statement Keywords
      • lock Statement

위와 같은 구조로 Document에 구성되어 있다.

이 것의 동작 방식을 까보려면 역시 Decompile.

private void DoAdd(int value)
{
    lock (_collection)
    {
        _collection.Add(value);
    }
}

위 코드를 Decompile시 아래의 il로 해석된다.

.method private hidebysig instance void
    DoAdd(
      int32 'value'
    ) cil managed
  {
    .maxstack 2
    .locals init (
      [0] class [System.Collections]System.Collections.Generic.List`1<int32> V_0,
      [1] bool V_1
    )

    // [24 13 - 24 31]
    IL_0000: ldarg.0      // this
    IL_0001: ldfld        class [System.Collections]System.Collections.Generic.List`1<int32> Sample.LockSample::_collection
    IL_0006: stloc.0      // V_0
    IL_0007: ldc.i4.0
    IL_0008: stloc.1      // V_1
    .try
    {
      IL_0009: ldloc.0      // V_0
      IL_000a: ldloca.s     V_1
      IL_000c: call         void [System.Threading]System.Threading.Monitor::Enter(object, bool&)

      // [26 17 - 26 40]
      IL_0011: ldarg.0      // this
      IL_0012: ldfld        class [System.Collections]System.Collections.Generic.List`1<int32> Sample.LockSample::_collection
      IL_0017: ldarg.1      // 'value'
      IL_0018: callvirt     instance void class [System.Collections]System.Collections.Generic.List`1<int32>::Add(!0/*int32*/)

      // [27 13 - 27 14]
      IL_001d: leave.s      IL_0029
    } // end of .try
    finally
    {

      IL_001f: ldloc.1      // V_1
      IL_0020: brfalse.s    IL_0028
      IL_0022: ldloc.0      // V_0
      IL_0023: call         void [System.Threading]System.Threading.Monitor::Exit(object)

      IL_0028: endfinally
    } // end of finally

    // [28 9 - 28 10]
    IL_0029: ret

  } // end of method LockSample::DoAdd

결과를 토대로 lock 문 없이 재구성하면,

private void DoAdd2(int value)
{
    bool lockTaken = default;
    try
    {
        System.Threading.Monitor.Enter(_collection, ref lockTaken);
        _collection.Add(value);
    }
    finally
    {
        System.Threading.Monitor.Exit(_collection);
    }
}

위 코드와 거의 유사하다.

하지만 실제로 저 코드도 decompile 시 il이 완전히 동일하지는 않다.

il을 봤을 땐 lock 문 없는 아래쪽 코드가 더 효율적으로 보였다.
왜 그런진 컴파일러의 동작까지 알아야 할텐데 그건 아직 내 역량 밖..

il source
  
    .method private hidebysig instance void
    DoAdd2(
      int32 'value'
    ) cil managed
  {
    .maxstack 2
    .locals init (
      [0] bool lockTaken
    )

    // [32 13 - 32 38]
    IL_0000: ldc.i4.0
    IL_0001: stloc.0      // lockTaken
    .try
    {

      // [35 17 - 35 76]
      IL_0002: ldarg.0      // this
      IL_0003: ldfld        class [System.Collections]System.Collections.Generic.List`1 Sample.LockSample::_collection
      IL_0008: ldloca.s     lockTaken
      IL_000a: call         void [System.Threading]System.Threading.Monitor::Enter(object, bool&)

      // [36 17 - 36 40]
      IL_000f: ldarg.0      // this
      IL_0010: ldfld        class [System.Collections]System.Collections.Generic.List`1 Sample.LockSample::_collection
      IL_0015: ldarg.1      // 'value'
      IL_0016: callvirt     instance void class [System.Collections]System.Collections.Generic.List`1::Add(!0/*int32*/)

      // [37 13 - 37 14]
      IL_001b: leave.s      IL_0029
    } // end of .try
    finally
    {

      // [40 17 - 40 60]
      IL_001d: ldarg.0      // this
      IL_001e: ldfld        class [System.Collections]System.Collections.Generic.List`1 Sample.LockSample::_collection
      IL_0023: call         void [System.Threading]System.Threading.Monitor::Exit(object)

      // [41 13 - 41 14]
      IL_0028: endfinally
    } // end of finally

    // [42 9 - 42 10]
    IL_0029: ret

  } // end of method LockSample::DoAdd2
  

Related Posts

View All Posts »

.NET 서버 메모리 누수 잡기 — dotnet-gcdump로 안 보이는 할당 폭탄

.NET 서버 메모리가 2분 만에 136MB에서 14GB로 폭증했다. dotnet-gcdump와 dotnet-counters로도 안 잡히는 할당 폭탄을 추적하고, 그 과정을 재사용 가능한 스킬로 만든 이야기.

#Aethelgard #AI #Claude #GameDev
Unity로 VR 악기 앱 만들기 — 루프 스테이션 1인 밴드 (Solo Band Studio)

Unity로 VR 악기 앱 만들기 — 루프 스테이션 1인 밴드 (Solo Band Studio)

Meta Quest용 VR 음악 창작 툴을 Unity로 개발한 과정. 피아노·드럼·베이스를 연주하고 루프 스테이션으로 혼자 합주를 완성한다. AI와 함께 한 달 만에 만든 대학 수업 프로젝트.

#Unity #VR #Audio #CSharp
실시간 멀티플레이 게임 아키텍처 회고 — 루니아 원정대 5년 후

실시간 멀티플레이 게임 아키텍처 회고 — 루니아 원정대 5년 후

모바일 실시간 멀티플레이 게임 루니아 원정대의 서버 5종 + 클라이언트 2종 전체 아키텍처를 설계했던 경험. 커스텀 ECS, 커맨드 동기화, 분산 서버, 재접속 시스템까지 — 5년이 지난 지금 그때의 기술 판단들을 돌아본다.

#Architecture #Retrospective #CSharp #Multiplay