알라딘MGG와이드바


Meyer's Singleton 쓸 때 주의할 점 개발 이야기

오래간만에 코드 이야기입니다.

Meyer's Singleton 를 쓰면 간단하게 싱글턴을 만들 수 있지만, 아래와 같은 실수를 하지 않도록 주의해야 합니다.


   1: class CTest {
   2: public:
   3:     static CTest& Inst() {
   4:         static CTest s;
   5:         return s;
   6:     }
   7:     const char* GetName(int i) {
   8:         return m_Name[i].c_str();
   9:         //return m_Name[i]->c_str();
  10:     }
  11:  
  12: private:
  13:     CTest() {
  14:         cout << "CTest construct start\n";
  15:         for (int i = 0; i < 3; ++i) {
  16:             m_Name[i] = string("Test");
  17:             //m_Name[i] = new string("test");
  18:             cout << i << endl;
  19:             SleepEx(100, FALSE);
  20:         }
  21:         cout << "CTest construct end\n";
  22:     }
  23:  
  24:     string m_Name[3];
  25:     //string* m_Name[10];
  26: };
  27:  
  28: unsigned __stdcall ThreadTest(void* pArguments) {
  29:     int nIndex = (int)pArguments;
  30:  
  31:     cout << "ThreadTest start : [" << nIndex << "]" << endl;
  32:     const char* pName = CTest::Inst().GetName(nIndex);
  33:     cout << "thread [" << nIndex << "]. name is : " << pName << endl;
  34:     cout << "ThreadTest end : [" << nIndex << "]" << endl;
  35:  
  36:     return 0;
  37: }
  38:  
  39: int _tmain(int argc, _TCHAR* argv[]) {
  40:     HANDLE hThreads[2];
  41:     unsigned threadID = 0;
  42:  
  43:     hThreads[0] = (HANDLE)_beginthreadex(NULL, 0, &ThreadTest, (void*)0, 0, &threadID);
  44:     hThreads[1] = (HANDLE)_beginthreadex(NULL, 0, &ThreadTest, (void*)2, 0, &threadID);
  45:  
  46:     WaitForMultipleObjects(2, hThreads, TRUE, INFINITE);
  47:     cin >> threadID;
  48:  
  49:     return 0;
  50: }

두 thread 가 동시에 싱글턴인 CTest 의 멤버변수를 접근하는 코드입니다.
출력은 아래와 같습니다. (실제로 해 보면 cout 내에서도 thread switching 이 발생해서 출력이 섞여서 보일 것입니다)


   1: ThreadTest start : [0]        // thread 0 시작
   2: CTest construct start        // CTest::Inst() singleton 생성 시작
   3: 0                            // CTest 생성자 for loop 0 번째
   4: ThreadTest start : [2]        // thread 2 시작
   5: thread [2]. name is :        // 생성자 실행중인 CTest 객체를 리턴받아서 출력
   6: ThreadTest end : [2]        // thread 2 종료
   7: 1                            // CTest 생성자 for loop 1 번째
   8: 2                            // CTest 생성자 for loop 2 번째
   9: CTest construct end            // CTest 생성자 완료
  10: thread [0]. name is : Test    // thread 0 에서 생성자 호출 완료된 CTest 객체로 작업
  11: ThreadTest end : [0]        // thread 0 종료

thread 0 이 시작된 후, CTest 생성자를 호출합니다.
하지만, 생성자에서 SleepEx 호출될 때 thread 2 로 task switch 됩니다.
문제는 thread 2 가 CTest::Inst().GetName() 할 때는 아직 thread 0 에서 생성자 작업이 한창중인 CTest 싱글턴 객체를 리턴해 버리는데 있습니다. 그래서, 출력 5 번에 보면 name is : 에 아무것도 보이지 않습니다.
thread 2 가 종료된 후, thread 0 은 계속 CTest 생성자 작업을 하고, 출력 10 번에서 정상적으로 생성자를 호출한 CTest 로 name is : Test 를 제대로 보여주고 있습니다. 만약 코드 25 번에서처럼 m_Name 을 포인터로 만들면, thread 2 가 초기화가 아직 안 된 m_Name 포인터에 접근하기 때문에 거의 언제나 crash 가 납니다.

이렇게 생성자 함수 실행 속도나 순서에 문제가 있는 싱글턴은, 어플리케이션이 처음 로딩될 때 한 번 호출도록 고치는 게 좋습니다. Meyer 도 effective c++ 2ed(Item 47: Ensure that non-local static objects are initialized before they're used.) 에서 그렇게 할 것을 권장하고 있습니다. (3판에서는 해당 내용이 빠졌네요.) More Effective C++ Item 26: Limiting the number of objects of a class 도 참고하시고, Linkage of inline functions 도 참고하면 좋을 듯.
그러니까, 코드 43, 44 에서 _beginthreadex 하기 전에, CTest::Inst() 를 호출해, single-thread 상태에서 한 번 싱글턴을 호출해 미리 객체를 준비해 두는 것입니다.

CTest::Inst(); 만 할 경우 release 에서 최적화 하면서 코드를 제거할 수도 있으니, 최적화 안 될만한 작업을 해 주는 것도 좋습니다. -> 이 부분은 최적화되지 않는다고 하네요. 좀 더 실험할 기회가 있다면 다시 포스팅 해 보도록 하겠습니다.

덧글

  • 2010/01/28 00:04 # 삭제 답글 비공개

    비공개 덧글입니다.
  • 2010/01/28 10:11 # 삭제 답글 비공개

    비공개 덧글입니다.
  • 박PD 2010/01/28 10:29 #

    긴가민가 했었는데, 좋은 정보 감사합니다. :)
  • object 2010/01/28 15:44 # 답글

    제가 본문의 의도를 약간 잘 이해를 못하겠는데요, 이 코드는 전혀 스레드 안전하지 않아 위험할 수밖에 없습니다. 보여주신 출력 예는 전형적인 data race이고요. 만약 싱글톤이 오직 한 스레드가 안전하게 초기화를 한 뒤, 그 뒤로 read-only로만 읽힌다면 락 없이도 문제 없이 쓸 수 있습니다. 그런데, 생성자체도 한 스레드가 안전하게 한다는 걸 보장하기가 힘듭니다. 사실 대부분은 잘 되긴 하겠지만요. 그래서 나온 패턴이 double-checked locking인데, 이것도 아주 엄밀하게 따져서 정확하게 안 돌아갈 수 있습니다만, VC++ 2005 이상에서는 어떻게 안전하게 만들 수 있습니다. http://en.wikipedia.org/wiki/Double-checked_locking
  • object 2010/01/28 16:06 #

    아하 ㅎㅎ 네네.. 전 멀티스레드 안전하지 않은 코드를 주의하자는 말이 너무 당연하게 들려서 잘 이해가 안 되었나 봅니다. 그런데 사실 이거 잘 알아도 막상 짜다보면 올려주신 코드 같은 곳에서 실수 많이 합니다.
  • 박PD 2010/01/28 16:06 #

    '위 코드는 안전하지 않으므로 주의하자' 고 쓴 글입니다.
    전달이 잘 안 되나 보네요. T_T
  • 2010/01/30 02:00 # 삭제 답글 비공개

    비공개 덧글입니다.
  • 박PD 2010/01/30 10:49 #

    msn 으로 연락 주세요~
댓글 입력 영역


Yes24위대한게임의탄생3

위대한 게임의 탄생 3
예스24 | 애드온2