Microsoft MVP성태의 닷넷 이야기
VC++: 39. C++에서 싱글톤 구현하기 [링크 복사], [링크+제목 복사],
조회: 34522
글쓴 사람
정성태 (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)
1174정성태11/15/201126521.NET Framework: 269. 일반 참조형의 기본 메모리 소비는 얼마나 될까요? [4]
1173정성태11/14/201122747.NET Framework: 268. .NET Array는 왜 12bytes의 기본 메모리를 점유할까? [1]
1172정성태11/13/201119693.NET Framework: 267. windbg - GC Heap에서 .NET 타입에 대한 배열을 찾는 방법
1171정성태11/12/201136382.NET Framework: 266. StringBuilder에서의 OutOfMemoryException 오류 원인 분석 [4]파일 다운로드1
1170정성태11/10/201125591.NET Framework: 265. Named 동기화 개체 생성 시 System.UnauthorizedAccessException 예외 발생하는 경우
1169정성태11/10/201129398.NET Framework: 264. 다중 LAN 카드 환경에서 Dns.GetHostAddresses(local)가 반환해 주는 IP의 우선순위는 어떻게 될까요? [4]
1168정성태11/6/201125251오류 유형: 139. TlbImp : error TI0000 : A single valid machine type compatible with the input type library must be specified
1167정성태11/5/201137096개발 환경 구성: 133. Registry 등록 과정 없이 COM 개체 사용 - 두 번째 이야기 [5]파일 다운로드4
1166정성태11/5/201123114.NET Framework: 263. byte[] pData = new byte[100000]로 인한 성능 차이? [1]파일 다운로드1
1165정성태11/3/201128021개발 환경 구성: 132. "Visual Studio Command Prompt (2010)" 명령행에서 2.0 버전의 MSBuild를 구동하는 방법 [2]파일 다운로드1
1164정성태11/1/201126196.NET Framework: 262. .NET 스레드 콜 스택 덤프 (4) - .NET 4.0을 지원하지 않는 MSE 응용 프로그램 원인 분석
1163정성태10/31/201125707.NET Framework: 261. .NET 스레드 콜 스택 덤프 (3) - MSE 소스 코드 개선파일 다운로드1
1162정성태10/30/201125836.NET Framework: 260. .NET 스레드 콜 스택 덤프 (2) - Managed Stack Explorer 소스 코드를 이용한 스택 덤프 구하는 방법파일 다운로드1
1161정성태10/29/201122615.NET Framework: 259. Type.GetMethod - System.Reflection.AmbiguousMatchException파일 다운로드1
1159정성태10/28/201126066.NET Framework: 258. Roslyn 맛보기 - SyntaxTree 조작 [2]
1158정성태10/24/201125371.NET Framework: 257. Roslyn 맛보기 - Roslyn Symbol / Binding API파일 다운로드1
1157정성태10/23/201129849.NET Framework: 256. Roslyn 맛보기 - Syntax Analysis (Roslyn Syntax API) [2]
1156정성태10/23/201128290.NET Framework: 255. Roslyn 맛보기 - Roslyn Services APIs를 이용한 Code Issue 및 Code Action 기능 소개 [1]
1155정성태10/22/201126360.NET Framework: 254. Roslyn 맛보기 - C# Interactive (2)
1154정성태10/22/201133088.NET Framework: 253. Roslyn 맛보기 - C# Interactive (1)
1153정성태10/21/201141961.NET Framework: 252. Roslyn 맛보기 - C# 소스 코드를 스크립트처럼 다루는 방법 [7]파일 다운로드1
1152정성태10/20/201123641.NET Framework: 251. string.GetHashCode는 hash 값을 cache 할까?
1151정성태10/18/201122577Java: 13. 자바도 64비트에서 (2GB) OutOfMemoryException 예외가 발생할까?
1150정성태10/18/201129833.NET Framework: 250. WPF - ComboBox의 SelectionChagned 이벤트파일 다운로드1
1149정성태10/16/201125464.NET Framework: 249. WPF - d:DesignHeight 값을 구할 수 있을까?
1148정성태10/14/201131453Java: 12. 자바에서 LINQ 사용? [7]
... 151  152  153  154  [155]  156  157  158  159  160  161  162  163  164  165  ...