← 문제로

20. lock / Monitor 의 내부 동작: 동기화 블록·재진입·잠금 대상

난이도 중
내 리뷰 · C#
해설 · C#

해설 — lock / Monitor 의 내부 동작: 동기화 블록·재진입·잠금 대상

난이도: 중상

요약

lock (o) { ... }Monitor.Enter(o)Monitor.Exit(o) 로 확장된다. 잠금은 대상 객체의 동기화 블록(sync block) 에 붙고, "소유 스레드 + 재귀 카운트" 로 관리되므로 같은 스레드는 재진입할 수 있다. 정확성은 전적으로 잠금 대상의 객체 동일성에 달려 있어, 매번 다른 객체(박싱된 값)에 잠그면 상호 배제가 깨지고, 전역 공유 객체(인터닝 문자열·this·typeof)에 잠그면 무관한 코드와 교착·호위가 난다. 예측:

  • Q1: 교착하지 않는다(재진입 OK).
  • Q2 (A): 상호 배제가 안 된다(매 호출 서로 다른 박스 객체).
  • Q3 (B): 인터닝 문자열은 프로세스 전역 공유 → 무관한 코드가 같은 문자열을 잠그면 교착·경합.
  • Q4: 외부 코드가 같은 인스턴스를 잠글 수 있어 교착·잠금 제어 상실.

핵심 개념

  • 동기화 블록 인덱스(sync block index): 모든 참조 객체의 헤더에는 필드가 있고, 잠금이 필요해지면 런타임이 그 객체를 sync block 테이블 엔트리에 연결한다(경합 없을 땐 헤더에 얇은 잠금/thin lock 으로 두다가, 경합·대기·해시코드 필요 시 sync block 으로 승격). 이 엔트리가 소유 스레드 ID 와 재귀 카운트, 대기 큐를 담는다.
  • Monitor 의미: Enter 는 소유 스레드가 자신이면 카운트만 +1(재진입), 아니면 대기. Exit 는 카운트 −1, 0이 되면 해제. locktry/finally 로 예외 시에도 Exit 를 보장.
  • 잠금 대상은 "값이 아니라 객체 동일성": 두 참조가 같은 객체를 가리켜야 상호 배제가 성립한다.

단계별 내부 동작

  • Q1 재진입: Outer_gate 를 잡은 스레드가 Inner 에서 다시 _gateEnter 하면, 소유자가 자기 자신이므로 재귀 카운트만 증가한다. 교착 없음. Exit 가 두 번 호출돼 카운트가 0으로 돌아가며 해제. (C++ 의 비재귀 std::mutex 와 대비되는 지점.)
  • Q2 (A) 박싱: lock ((object)key) 는 값 형식 int매 호출 새 박스 객체로 힙에 할당한다. 두 스레드가 같은 key 값을 넘겨도 서로 다른 박스를 잠그므로 상호 배제가 성립하지 않는다(사실상 잠금 없음). 값을 키로 삼아 세밀 잠금을 하려면 int→object 잠금 객체를 ConcurrentDictionary 등에 캐싱해 동일 객체를 재사용해야 한다.
  • Q3 (B) 인터닝 문자열: 문자열 리터럴은 인터닝되어 프로세스 전역에서 같은 인스턴스가 공유된다. 서로 다른 어셈블리/모듈의 무관한 코드가 우연히 같은 문자열을 잠그면, 의도치 않게 같은 잠금을 공유해 교착·잠금 호위가 생긴다. 문자열을 잠금 대상으로 쓰지 말 것.
  • Q4 lock(this): 인스턴스가 외부에 공개돼 있으면 호출자도 lock(instance) 를 할 수 있다. 내부와 외부가 같은 잠금을 두고 순서가 어긋나면 교착하고, 잠금 정책을 캡슐화하지 못해 제어를 잃는다. typeof(X) 잠금도 같은 이유로 위험(타입 객체는 전역 공유). 해법: 비공개 전용 잠금 객체(private readonly object _gate = new())를 둔다.

흔한 오해·함정

  • "lock 은 값을 잠근다" → 아니다. 객체 동일성을 잠근다. 박싱/문자열/구조체 복사가 끼면 대상이 매번 달라진다.
  • 재진입을 당연시 → Monitor 는 재진입이지만, 재진입 중 상태 불변식이 깨진 채 재귀 호출하면 논리 버그가 숨는다. C++ std::mutex 로 포팅하면 같은 코드가 자기 교착한다.
  • 공개 객체 잠금(this, typeof, 인터닝 문자열) → 외부와 잠금 공유로 교착/호위. 항상 전용 비공개 잠금.
  • 값 형식 필드에 lock → 컴파일 경고/에러 또는 매번 박싱. 잠금 대상은 참조 타입 전용.
  • 긴 임계 구역/보유 중 블로킹 I/O → 잠금 호위. 임계 구역 최소화.

면접 포인트

  1. Monitor = 동기화 블록 + 소유 스레드 + 재귀 카운트 → 재진입 가능(C++ std::mutex 비재귀와의 차이)과 그 함정.
  2. 잠금은 값이 아니라 객체 동일성에 붙는다 — 박싱된 값·복사된 구조체·매번 새 객체를 잠그면 상호 배제가 소리 없이 깨진다.
  3. this/typeof/인터닝 문자열 같은 전역 공유 객체 잠금 금지, 전용 비공개 잠금 사용, 그리고 값 키 세밀 잠금 시 잠금 객체 재사용(캐싱) 패턴.