Microsoft MVP성태의 닷넷 이야기
VC++: 39. C++에서 싱글톤 구현하기 [링크 복사], [링크+제목 복사],
조회: 34517
글쓴 사람
정성태 (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분
정성태

... 151  [152]  153  154  155  156  157  158  159  160  161  162  163  164  165  ...
NoWriterDateCnt.TitleFile(s)
1249정성태2/26/201230988.NET Framework: 311. .NET 스레드 콜 스택 덤프 (5) - ICorDebug 인터페이스 사용법 [2]파일 다운로드3
1248정성태2/25/201242474.NET Framework: 310. C#의 Shift 비트 연산 정리파일 다운로드1
1247정성태2/25/201225196.NET Framework: 309. .NET 응용 프로그램에 기본 생성되는 스레드들에 대한 탐구 [1]파일 다운로드1
1246정성태2/25/201224772개발 환경 구성: 145. 한영 변환은 되지만, 정작 한글 입력이 안되는 경우
1245정성태2/25/201235476개발 환경 구성: 144. 윈도우에서도 유닉스처럼 명령행으로 원격 접속하는 방법
1244정성태2/24/201232646.NET Framework: 308. .NET System.Threading.Thread 개체에서 Native Thread Id를 구할 수 있을까? [1]파일 다운로드1
1243정성태2/23/201232606개발 환경 구성: 143. Visual Studio 2010 - .NET Framework 소스 코드 디버깅 - 두 번째 이야기 [1]
1242정성태2/20/201239468VC++: 58. API Hooking - 64비트를 고려해야 한다면? EasyHook! [7]파일 다운로드1
1241정성태2/20/201226307.NET Framework: 307. .NET 4.0 응용 프로그램을 위한 ILMerge
1240정성태2/19/201232568디버깅 기술: 48. C/C++ JNI DLL을 Visual Studio로 디버깅하는 방법 [2]
1239정성태2/19/201224217.NET Framework: 306. 컴퓨터에 실행된 프로세스 중에 닷넷 응용 프로그램임을 알 수 있는 방법 - C# [1]파일 다운로드1
1238정성태2/19/201228103.NET Framework: 305. GetPrivateProfileSection / WritePrivateProfileSection의 C# 버전파일 다운로드1
1237정성태2/18/201232407개발 환경 구성: 142. Windows Embedded POSReady 7 설치 [1]
1236정성태2/17/201228233개발 환경 구성: 141. Windows 2008 R2 RDP 라이선스 서버 설치하는 방법
1235정성태2/16/201226684.NET Framework: 304. Hyper-V의 가상 머신을 C#으로 제어하는 방법 [1]파일 다운로드1
1234정성태2/16/201227100.NET Framework: 303. 원본 파일의 공백/라인을 유지한 체 XML 파일을 저장하는 방법 [1]파일 다운로드1
1233정성태2/16/201233185.NET Framework: 302. supportedRuntime 옵션과 System.BadImageFormatException 예외 [5]
1232정성태2/9/201229063VC++: 57. 웹 브라우저에서 Flash만 빼고 다른 ActiveX를 차단할 수 있을까? [3]파일 다운로드1
1231정성태2/8/201238622VC++: 56. Win32 API 후킹 - Trampoline API Hooking [5]파일 다운로드1
1230정성태2/6/201223964개발 환경 구성: 140. 프로젝트 생성 시부터 "Enable the Visual Studio hosting process" 옵션을 끄는 방법
1229정성태2/4/201228984.NET Framework: 301. P/Invoke의 성능을 높이기 위해 C++/CLI가 선택되려면? [5]파일 다운로드1
1228정성태2/4/201278306.NET Framework: 300. C#으로 만드는 음성인식/TTS 프로그램 [47]파일 다운로드1
1227정성태2/3/201229192.NET Framework: 299. 해당 어셈블리가 Debug 빌드인지, Release 빌드인지 알아내는 방법파일 다운로드1
1226정성태1/28/201270090.NET Framework: 298. 홀 펀칭(Hole Punching)을 이용한 Private IP 간 통신 - C# [15]파일 다운로드3
1225정성태1/24/201225693.NET Framework: 297. 특정 EXE 파일의 실행을 Internet Explorer처럼 "Protected Mode"로 실행하는 방법 [1]파일 다운로드1
1224정성태1/21/201237211개발 환경 구성: 139. 아마존 EC2에 새로 추가된 "1년 무료 Windows 서버 인스턴스"가 있다는데, 직접 만들어 볼까요? ^^ [11]
... 151  [152]  153  154  155  156  157  158  159  160  161  162  163  164  165  ...