Microsoft MVP성태의 닷넷 이야기
VC++: 39. C++에서 싱글톤 구현하기 [링크 복사], [링크+제목 복사],
조회: 34527
글쓴 사람
정성태 (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)
1148정성태10/14/201131453Java: 12. 자바에서 LINQ 사용? [7]
1147정성태10/13/201127352.NET Framework: 248. 닷넷에서 지원되는 문자열 인코딩 이름 목록
1146정성태10/12/201133075.NET Framework: 247. LINQ에서의 Max 기능 구현 [10]파일 다운로드1
1144정성태10/10/201128794.NET Framework: 246. WCF - 서버 측에서의 유효한 Timeout 설정파일 다운로드1
1143정성태10/9/201134378.NET Framework: 245. ASP.NET 서버 측 코드에서 페이스북 계정 연동하는 방법
1142정성태10/8/201134854.NET Framework: 244. 윈도우 폼을 열고 닫는 것만으로 메모리 leak이 발생할까? [2]파일 다운로드1
1141정성태10/7/201133478.NET Framework: 243. DataTable에 대해서 Dispose 메서드를 호출할 필요가 있을까? [4]파일 다운로드1
1140정성태10/6/201126631.NET Framework: 242. 닷넷 개발자 입장에서 이해해 보는 자바의 서블릿, JSP
1138정성태10/1/201144290Java: 11. 웹 로직에서 MS-SQL 서버 연결 [2]
1137정성태9/30/201129841Java: 10. 닷넷 개발자가 설치해 본 Oracle WebLogic Server - 설치 및 기본 도메인 구성
1136정성태9/29/201125269개발 환경 구성: 131. Visual Studio - ASP.NET의 Code-behind처럼 cs 파일을 그룹핑하는 매크로 함수 [2]파일 다운로드1
1135정성태9/29/201122718오류 유형: 138. TF10216: Team Foundation services are currently unavailable
1134정성태9/27/201130188.NET Framework: 241. C# 5.0에 새로 추가된 Caller Info 특성 [5]
1133정성태9/25/201133491VC++: 54. C++로 만든 WinRT 프로그램 [2]
1132정성태9/24/201172959Java: 9. 자바의 keytool.exe 사용법과 Tomcat의 SSL 통신 설정
1131정성태9/23/201129154Java: 8. 닷넷 개발자가 구현해 본 자바 웹 서비스 (2)
1130정성태9/23/201137136Java: 7. 닷넷 개발자가 구현해 본 자바 웹 서비스 (1)파일 다운로드2
1129정성태9/22/201128802개발 환경 구성: 130. Hyper-V에 MS-DOS VM 만드는 방법 - MSDN 구독자 대상 [3]
1128정성태9/20/201128954오류 유형: 137. KB2449742 보안 업데이트로 인한 충돌 문제 해결 - 두 번째 이야기
1127정성태9/19/201133015Java: 6. Java에서 MySQL 사용 [2]
1126정성태9/18/201128165Math: 3. "유클리드 호제법"과 "Bezout's identity" 구현 코드(C#)파일 다운로드1
1125정성태9/17/201126062Windows: 54. Windows 8 개발자 Preview를 사용해 보고... [2]
1124정성태9/17/201126391.NET Framework: 240. System.Collections.ArrayList가 .NET 4.5에서 지원이 안된다??? [2]
1123정성태9/17/201165125Windows: 53. 2가지 모드의 Internet Explorer 10과 ActiveX [6]
1122정성태9/16/201132902Windows: 52. 새롭게 지원되는 WinRT 응용 프로그램 [7]
1121정성태9/12/201127590Java: 5. WTP 내에서 서블릿을 실행하는 환경
... 151  152  153  154  155  [156]  157  158  159  160  161  162  163  164  165  ...