Microsoft MVP성태의 닷넷 이야기
VC++: 39. C++에서 싱글톤 구현하기 [링크 복사], [링크+제목 복사],
조회: 31017
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
(연관된 글이 4개 있습니다.)
C++에서 싱글톤 구현하기


우연히 아래의 글을 읽게 되었습니다. (사실 이제는 거의 .NET에서 놀고 있지만, 여전히 C/C++을 관심 범위에 두고는 있습니다. ^^)

C++에서 싱글톤 구현하기
; (broken) http://agbird.egloos.com/4730538

일단, 위의 글과 함께 댓글들을 읽어보면서 한가지 놀란 점이 있습니다. 의외로 "Thread-Safe"하냐는 것에 대해서는 아무도 관심을 갖고 있지 않았기 때문입니다.

알려진 바에 의하면, 초기화 순서가 불분명하다는 단점에도 불구하고 Singleton과 같이 구현하는 경우에는 Thread-safe합니다. 하지만, 겉으로 좋아보이는 DynamicSingleton의 경우에는 Thread-safe하지 않다는 다소 치명적인 단점이 있습니다.

이 문제를 해결하는 것이 생각보다 만만치 않은데 아래의 문서에서 아주 자세히 설명해 주고 있습니다.

C++ and the Perils of Double-Checked Locking
; http://www.aristeia.com/Papers/DDJ_Jul_Aug_2004_revised.pdf

DynamicSingleton을 위의 문서대로 thread-safe하게 다시 구현해 볼까요? ^^

우선, 잠금을 통해서 Thread-safe하게 바꿉니다.

class DynamicSingleton {
  private:
  ... [생략] ...
  public:
    static DynamicSingleton* getInstance() 
    {
      Lock lock;
      if (inst == 0) inst = new DynamicSingleton();
      return inst;
    }
};

하지만, 매번 getInstance 때마다 스레드 경합이 발생할 여지를 두는 것은 좋지 않기 때문에 다음과 같이 DCLP(Double Chceked Locking Pattern)을 이용해서 개선할 수 있습니다.

static DynamicSingleton* getInstance() 
{
  if (inst == 0)
  {
    Lock lock;
    if (inst == 0) 
    {
      inst = new DynamicSingleton();
    }
    return inst;
  }
}

그래도 여전히 헛점이 있습니다. 명령어 재배열과 같은 최적화 기법에 의해 "inst = new DynamicSingleton"이라는 구문에 오류를 발생할 수 있는 여지가 존재하게 됩니다. 즉, 생성자가 불리지 않았음에도 불구하고 inst 변수에 값이 할당되어져 있는 상황이 발생하게 되고 연이은 스레드의 호출에서 생성자가 호출되지 않는 상태의 그 inst 변수가 사용되어져 버리는 것입니다. 그래서 다음과 같이 바뀌어야 합니다.

class DynamicSingleton
{
private:
    static volatile DynamicSingleton* volatile inst;

    static volatile DynamicSingleton* volatile getInstance() 
    {
      if (inst == 0)
      {
        Lock lock;
        if (inst == 0) 
        {
          volatile DynamicSingleton* volatile temp = new volatile DynamicSingleton;
          inst = temp;
        }
        return inst;
      }
    }
}

위와 같이 volatile로 하는 경우 말고도 다중 프로세서에서의 메모리 캐시 문제를 해결하기 위해 Memory Barrier를 직접 사용하는 방법을 택하는 것도 가능합니다. 그래서... 다음과 같이 구현해도 됩니다.

#include <intrin.h>
#pragma intrinsic(_ReadWriteBarrier)

class DynamicSingleton
{
private:
    static DynamicSingleton* inst;

    static DynamicSingleton* getInstance() 
    {
      DynamicSingleton* volatile temp = inst;
      _ReadWriteBarrier();
      if (temp == 0)
      {
        Lock lock;
        temp = inst;
        if (temp == 0) 
        {
          temp = new DynamicSingleton;
          _ReadWriteBarrier();
          inst = temp;
        }
        return inst;
      }
    }
}

PhoenixSingleton은, 바로 위의 코드와 같이 Thread-safe 문제를 어느 정도 해결한 상태에서 살을 붙여야 할 테니... "C/C++에서 thread-safe한 Singleton 개체"를 사용하는 것이 그다지 녹록치만은 않습니다. (사실 이것은 C/C++만의 문제는 아닙니다.)

위의 PDF 문서에 의하면, (학문적으로 파고들기에는 제가 실력이 모자라서 이해하기 힘들지만) C++의 abstract machine 자체가 단일 스레드이기 때문에 위와 같은 소스 코드의 보정에도 불구하고 thread-unsafe한 기계어를 생산하는 C++ 컴파일러도 있다고 합니다. VC++이 그런 경우에 포함되는 것인지는 확인할 길이 없고. (이런 거 보면... 저도 어쩔 수 없는 "응용 개발자"에 속해 있지요. ^^ 그래서 가끔 이런 것들을 학문적으로 파헤치는 분들이 부러울 때가 있습니다.)

그나저나, abstract machine이 다중 스레드인 언어는 그럼 또 뭐가 있는 건가요?




참고로, "C++에서 싱글톤 구현하기"에서 소개된 코드 중에 "명시적인 해제 작업을 피하기 위해서는 static 지역 객체를 사용하면 됩니다" 라면서 소개한 코드가 있는데요. 그것 역시 Thread-safe하지 않다는 문제가 있습니다. 이에 대해서는 다음의 글에서 설명되어져 있습니다.

C++ scoped static initialization is not thread-safe, on purpose!
; https://devblogs.microsoft.com/oldnewthing/20040308-00/?p=40363

재미있게도, scoped-static 초기화가 C++ 표준에 의해서 구현되었기 때문에 thread-safe하지 않은 것이 일단은 당연하다고 되어 있는데 댓글들을 보면 C++ 표준이 아닌 마이크로소프트 임의 구현이라는 비난이 있습니다.

표준이든 아니든, VC++은 일단 그렇게 구현되어 있기 때문에 주의하는 것이 좋겠습니다. ^^
(그런데, 위의 글이 2004년도에 씌여진 것이라서 VC++10 에서는 어떻게 바뀌었을지... 모를 일이군요)




끝으로. 한가지 더!

만약 저한테 Thread-safe한 C/C++ 코드를 만들라고 하면.
차라리 주 Thread-safe 개체와 그에 종속된 Thread-safe한 개체를 만들도록 기본 방침을 정할 것 같습니다. 무슨 소리냐면. 전역적인 static 초기화가 Thread-safe 함에도 불구하고 그 순서를 알 수 없다는 이유로 때로 사용이 기피되는데 이것만이 그 이유라면 다음과 같은 식으로 해결할 수 있다는 것입니다.

// ThreadSafeSingleton 개체는 전역 static으로 등록하고.
class ThreadSafeSingleton
{
   ThreadSafeSingleton()
   {
       // 순서 대로!
       SingletonA()->Initialize();
       SingletonB()->Initialize();
       SingletonC()->Initialize();
   }
}

물론, 초기화 비용이 큰 singleton 개체라면 어쩔 수 없이 지금까지 설명한 복잡한 방법대로 해야겠지만.

(첨부된 문서는 C++ and the Perils of Double-Checked Locking 문서를 다운로드 받아서 첨부한 것입니다.)



[이 토픽에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 3/22/2023]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 



2010-11-22 12시40분
[손호용] 동기화 Lock에 대한 UnLock이 빠진건 아닌가요?
[guest]
2010-11-23 11시19분
아... 위의 글에서 Lock lock; 코드를 지적하시는 것 같은데, 그건 별도의 사용자 정의 클래스로 생성자에서 lock, 파괴자에서 unlock 하는 코드가 있다는 것을 가정합니다.
정성태
2016-10-21 06시53분
[guest] 답글이 달릴지는 모르겠지만 저 Lock 객체는 내부적으로 CriticalSection을 사용하나요??
C++에서 CriticalSection을 사용하는데도 reordering 문제가 발생한다는 말씀인가요?
저 Lock의 정체가 궁금합니다..
[guest]
2016-10-24 12시29분
소스코드가 없는데 아마도 CriticalSection이 아닐까요? 사실 CPU 입장에서는 CriticalSection의 코드가 사용된다고 해서 reordering을 하지 말라는 단서가 없으니 본문의 내용이 맞을 듯합니다. 그게 아니라면 Visual C++ 컴파일러가 CriticalSection이 사용되었다면 CPU 명령어 처리를 직렬화시키는 코드를 일부러 삽입해야 하는데... 글쎄요. 이 부분은 확인해야 알 수 있겠군요. ^^
정성태
2016-10-26 07시43분
[guest] 안녕하세요! 친절하게 답변 주셔서 감사합니다! 그 이후로 조금더 찾아보고 글을 써봅니다.
위에서 링크걸어놓으신 http://www.aristeia.com/Papers/DDJ_Jul_Aug_2004_revised.pdf의 사례를 보면 C++ Lock에서 메모리 베리어가 없어서 reordering이 발생하는것으로 설명되어 있습니다. 그래서 메모리 베리어를 넣어야한다는 내용이 포함되어 있는데..
https://docs.microsoft.com/en-us/windows/win32/sync/synchronization-and-multiprocessor-issues 이 글을 보면 The following synchronization functions use the appropriate barriers to ensure memory ordering:
Functions that enter or leave critical sections 라는 글귀가 있으며, EnterCriticalSection의 디스어셈블 결과 lock prefix가 사용되는 것을 확인하였습니다. 역시 CriticalSection은 베리어 기능을 제공하고 있는것 같습니다. 그렇다면 저 기사의 Lock의 존재는 무엇일까요? ㅠ 컴파일러 버전에 따라 베리어의 존재 유무가 갈리는걸까요? ㅠㅜㅠ DCLP를 쓰면 안된다는 내용에서 출발한 의문이 여기까지 와버렸네요 ㅠ 괜히 귀찮게 해드려 죄송합니다
[guest]
2016-10-26 12시30분
아... 제가 윈도우의 CS(Critical Section)이 barrier 역할이 된다는 것을 잊고 있었군요.

Synchronization and Multiprocessor Issues
; https://learn.microsoft.com/en-us/windows/win32/sync/synchronization-and-multiprocessor-issues

일단, 그렇다면 그 PDF 문서는 (플랫폼을 특정하지 않은) C++ 언어 수준에서 이야기한 것이기 때문에 Lock lock; 내부에서 memory barrier 보장을 100% 하는 것은 아니라는 가정으로 예제 코드가 나온 것 같습니다. 따라서, (적어도) 윈도우 환경에서 Lock 클래스가 CS를 이용한 경우라면 굳이 memory barrier 관련 코드를 넣을 필요는 없겠습니다.
정성태
2016-10-26 04시53분
[guest] 아 C++ 언어 수준에서의 이야기라고 한다면 맞는 말이겠군요! 실마리를 못찾다가 이제야 이해가 갔습니다.
비루한 지식으로 이것저것 알아보려다보니 자주 막히네요 ㅎㅎ 변변찮은 질문에 친절히 답해주셔서 너무 감사드립니다!
수고하세요~
-초록불꽃
[guest]
2016-11-01 03시31분
정성태

1  2  3  [4]  5  6  7  8  9  10  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13722정성태9/2/20241725C/C++: 169. C/C++ - CRT(C Runtime) 함수에 의존성이 없는 프로젝트 생성
13721정성태8/30/20242127C/C++: 168. Visual C++ CRT(C Runtime DLL: msvcr...dll)에 대한 의존성 제거 - 두 번째 이야기
13720정성태8/29/20241976VS.NET IDE: 193. C# - Visual Studio의 자식 프로세스 디버깅
13719정성태8/28/20242258Linux: 79. C++ - pthread_mutexattr_destroy가 없다면 메모리 누수가 발생할까요?
13718정성태8/27/20242753오류 유형: 921. Visual C++ - error C1083: Cannot open include file: 'float.h': No such file or directory [2]
13717정성태8/26/20242417VS.NET IDE: 192. Visual Studio 2022 - Windows XP / 2003용 C/C++ 프로젝트 빌드
13716정성태8/21/20242210C/C++: 167. Visual C++ - 윈도우 환경에서 _execv 동작
13715정성태8/19/20242098Linux: 78. 리눅스 C/C++ - 특정 버전의 glibc 빌드 (docker-glibc-builder)
13714정성태8/19/20242431닷넷: 2295. C# 12 - 기본 생성자(Primary constructors) (책 오타 수정) [3]
13713정성태8/16/20242642개발 환경 구성: 721. WSL 2에서의 Hyper-V Socket 연동
13712정성태8/14/20242708개발 환경 구성: 720. Synology NAS - docker 원격 제어를 위한 TCP 바인딩 추가
13711정성태8/13/20243188Linux: 77. C# / Linux - zombie process (defunct process)파일 다운로드1
13710정성태8/8/20243286닷넷: 2294. C# 13 - (6) iterator 또는 비동기 메서드에서 ref와 unsafe 사용을 부분적으로 허용파일 다운로드1
13709정성태8/7/20243165닷넷: 2293. C# - safe/unsafe 문맥에 대한 C# 13의 (하위 호환을 깨는) 변화파일 다운로드1
13708정성태8/7/20242784개발 환경 구성: 719. ffmpeg / YoutubeExplode - mp4 동영상 파일로부터 Audio 파일 추출
13707정성태8/6/20243228닷넷: 2292. C# - 자식 프로세스의 출력이 4,096보다 많은 경우 Process.WaitForExit 호출 시 hang 현상파일 다운로드1
13706정성태8/5/20243591개발 환경 구성: 718. Hyper-V - 리눅스 VM에 새로운 디스크 추가
13705정성태8/4/20243594닷넷: 2291. C# 13 - (5) params 인자 타입으로 컬렉션 허용파일 다운로드1
13704정성태8/2/20243358닷넷: 2290. C# - 간이 dotnet-dump 프로그램 만들기파일 다운로드1
13703정성태8/1/20243670닷넷: 2289. "dotnet-dump ps" 명령어가 닷넷 프로세스를 찾는 방법
13702정성태7/31/20243600닷넷: 2288. Collection 식을 지원하는 사용자 정의 타입을 CollectionBuilder 특성으로 성능 보완파일 다운로드1
13701정성태7/30/20243606닷넷: 2287. C# 13 - (4) Indexer를 이용한 개체 초기화 구문에서 System.Index 연산자 허용파일 다운로드1
13700정성태7/29/20243309디버깅 기술: 200. DLL Export/Import의 Hint 의미
13699정성태7/27/20243447닷넷: 2286. C# 13 - (3) Monitor를 대체할 Lock 타입파일 다운로드1
13698정성태7/27/20243371닷넷: 2285. C# - async 메서드에서의 System.Threading.Lock 잠금 처리파일 다운로드1
13697정성태7/26/20243505닷넷: 2284. C# - async 메서드에서의 lock/Monitor.Enter/Exit 잠금 처리파일 다운로드1
1  2  3  [4]  5  6  7  8  9  10  11  12  13  14  15  ...