← 문제로

46. 함수 지역 정적 변수의 스레드 안전 지연 초기화("magic statics")

난이도 상
내 리뷰 · C++
해설 · C++

해설 — 함수 지역 정적 변수의 스레드 안전 지연 초기화("magic statics")

난이도: 상

요약

함수 지역 static 은 제어 흐름이 그 선언을 처음 통과할 때 딱 한 번 초기화된다(지연 초기화). 컴파일러는 숨은 가드(guard) 변수로 "초기화 완료" 를 표시하고, C++11 이후 이 가드 검사·초기화가 스레드 안전하도록 보장한다. 네임스페이스 범위 정적은 반대로 main 이전에 초기화된다.

출력

[init gEager]
start main
f() = [init counter]
[init local]
101
f() = 101
counter() = 100

핵심 개념

  • 지연 초기화(lazy init) 시점: gEager 는 네임스페이스 범위라 프로그램 시작(정적 초기화 단계), 즉 main 진입 전에 초기화되어 [init gEager] 가 start main 보다 먼저 찍힌다. 반면 counter()/f() 안의 static 은 처음 호출될 때 초기화되므로 첫 f() 호출에서야 [init counter], [init local] 이 찍힌다.
  • 정확히 한 번: f() 를 다시 불러도 local 은 재초기화되지 않아 두 번째 f() 는 101 만 출력한다. counter() 의 c 도 한 번만 초기화되어 100.
  • 초기화 순서: local = counter() + makeVal("local", 1) 에서 counter() 가 먼저 평가되어 [init counter](c=100) 후 [init local](우변 1) → local = 101.

단계별 내부 동작

  1. 컴파일러는 각 함수 지역 static 마다 가드 변수(예: __guard_for_local)를 만든다.
  2. 함수 진입 시 생성된 코드는 대략:
    if ((guard & 1) == 0) {            // 아직 초기화 안 됨
        if (__cxa_guard_acquire(&guard)) {   // 락 획득(경쟁 시 한 스레드만)
            초기화 실행;               // 예외 나면 __cxa_guard_abort
            guard |= 1;
            __cxa_guard_release(&guard);      // 대기 스레드 깨움
        }
    }
    
  3. 스레드 안전(C++11 [stmt.dcl]p4): 여러 스레드가 동시에 처음 진입하면 하나만 초기화를 수행하고 나머지는 __cxa_guard_acquire 에서 완료를 대기한다. 초기화가 끝나면 모두 같은 인스턴스를 본다. 그래서 함수 지역 static 은 락 없는(사용자 코드 기준) 스레드 안전 싱글턴의 표준 관용구(Meyers Singleton)다.
  4. 소멸은 프로그램 종료 시 초기화의 역순으로 atexit 류에 등록되어 일어난다.

흔한 오해·함정

  • "static 은 프로그램 시작 시 다 초기화된다" — 함수 지역 static 은 아니다. 첫 통과 시점이며, 호출되지 않으면 초기화조차 안 된다.
  • 재진입 데드락/UB: 초기화 코드가 자기 자신의 초기화가 끝나기 전에 같은 static 을 재귀적으로 사용하면 표준상 미정의 동작이며, 구현에 따라 가드에서 데드락이 난다.
  • C++11 이전: 이 스레드 안전 보장이 없었다. 옛 코드/일부 임베디드 툴체인(-fno-threadsafe-statics) 에서는 이중 초기화·경쟁이 가능하므로 그 환경에선 직접 동기화가 필요하다.
  • 정적 초기화 순서 문제(SIOF): 서로 다른 번역 단위의 네임스페이스 범위 정적끼리는 초기화 순서가 정해지지 않는다. 이를 피하려 "함수가 지역 static 을 돌려주는(construct-on-first-use)" 패턴을 쓴다.
  • -fno-threadsafe-statics 트레이드오프: 가드 비용(초기화 후엔 원자적 로드 한 번)이 싫으면 끄지만 그러면 스레드 안전을 잃는다.

면접 포인트

  • Meyers Singleton 이 왜 스레드 안전한가(C++11 가드 변수·__cxa_guard_*)와 초기화 시점.
  • 네임스페이스 정적(프로그램 시작) vs 함수 지역 정적(첫 사용)의 초기화·소멸 시점 차이와 SIOF 회피.
  • 초기화 중 재귀 사용이 왜 UB/데드락인지, 그리고 소멸 순서(초기화 역순).