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