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

... 16  17  18  19  20  21  [22]  23  24  25  26  27  28  29  30  ...
NoWriterDateCnt.TitleFile(s)
13098정성태7/13/20226440개발 환경 구성: 647. Azure - scale-out 상태의 App Service에서 특정 인스턴스에 요청을 보내는 방법 [1]
13097정성태7/12/20225805오류 유형: 817. Golang - binary.Read: invalid type int32
13096정성태7/8/20228698.NET Framework: 2030. C# 11 - UTF-8 문자열 리터럴
13095정성태7/7/20226740Windows: 208. AD 도메인에 참여하지 않은 컴퓨터에서 Kerberos 인증을 사용하는 방법
13094정성태7/6/20226504오류 유형: 816. Golang - "short write" 오류 원인
13093정성태7/5/20227370.NET Framework: 2029. C# - HttpWebRequest로 localhost 접속 시 2초 이상 지연
13092정성태7/3/20228343.NET Framework: 2028. C# - HttpWebRequest의 POST 동작 방식파일 다운로드1
13091정성태7/3/20227276.NET Framework: 2027. C# - IPv4, IPv6를 모두 지원하는 서버 소켓 생성 방법
13090정성태6/29/20226327오류 유형: 815. PyPI에 업로드한 패키지가 반영이 안 되는 경우
13089정성태6/28/20226802개발 환경 구성: 646. HOSTS 파일 변경 시 Edge 브라우저에 반영하는 방법
13088정성태6/27/20225773개발 환경 구성: 645. "Developer Command Prompt for VS 2022" 명령행 환경의 폰트를 바꾸는 방법
13087정성태6/23/20228819스크립트: 41. 파이썬 - FastAPI / uvicorn 호스팅 환경에서 asyncio 사용하는 방법 [1]
13086정성태6/22/20228266.NET Framework: 2026. C# 11 - 문자열 보간 개선 2가지파일 다운로드1
13085정성태6/22/20228359.NET Framework: 2025. C# 11 - 원시 문자열 리터럴(raw string literals)파일 다운로드1
13084정성태6/21/20226791개발 환경 구성: 644. Windows - 파이썬 2.7을 msi 설치 없이 구성하는 방법
13083정성태6/20/20227435.NET Framework: 2024. .NET 7에 도입된 GC의 메모리 해제에 대한 segment와 region의 차이점 [2]
13082정성태6/19/20226494.NET Framework: 2023. C# - Process의 I/O 사용량을 보여주는 GetProcessIoCounters Win32 API파일 다운로드1
13081정성태6/17/20226519.NET Framework: 2022. C# - .NET 7 Preview 5 신규 기능 - System.IO.Stream ReadExactly / ReadAtLeast파일 다운로드1
13080정성태6/17/20227185개발 환경 구성: 643. Visual Studio 2022 17.2 버전에서 C# 11 또는 .NET 7.0 preview 적용
13079정성태6/17/20224798오류 유형: 814. 파이썬 - Error: The file/path provided (...) does not appear to exist
13078정성태6/16/20226990.NET Framework: 2021. WPF - UI Thread와 Render Thread파일 다운로드1
13077정성태6/15/20227207스크립트: 40. 파이썬 - PostgreSQL 환경 구성
13075정성태6/15/20226182Linux: 50. Linux - apt와 apt-get의 차이 [2]
13074정성태6/13/20226514.NET Framework: 2020. C# - NTFS 파일에 사용자 정의 속성값 추가하는 방법파일 다운로드1
13073정성태6/12/20226802Windows: 207. Windows Server 2022에 도입된 WSL 2
13072정성태6/10/20227076Linux: 49. Linux - ls 명령어로 출력되는 디렉터리 색상 변경 방법
... 16  17  18  19  20  21  [22]  23  24  25  26  27  28  29  30  ...