본문 바로가기
프로그래밍

멀티쓰레드 프로그래밍시 문제발생 시나리오

by ictlab 2009. 10. 21.

멀티 쓰레드 프로그래밍을 할때는 동작중인 모든 쓰레드가 동시에 돌아가고 있기 때문에
같은 데이터를 접근하는 쓰레드들은 철저하게 주의를 기울여야 합니다.
보통 같은 데이터 , 메모리 접근을 차단하기위해 싱크 오브젝트들을 사용하는데는
다음은 CriticalSection 사용중 발생한 문제점의 한 예 입니다.

실제 우리 회사 LogManager 프로젝트의 소스 코드입니다.

아래는 GUI Main 쓰레드에서 CopyData 메시지를 받아서 m_pTemp라는 StringList에서 로그 데이터를 추가하는 루틴입니다.
GUI 쓰레드에서 메시지를 받아 로그 데이터를 추가하고 별도의 태스크 쓰레드에서 List에 있는 로그 데이터를 데이터베이스(Firebird)에 INSERT 하는 구조로 되어있습니다.

 
      //----------------------------------------------------------------------------
      // Temp Log 데이터도 삭제 함 : String List에서 max count 초과 Log 삭제
      while( m_pTemp->Count > F_OptDlg->E_MaxLogLine->GetInt() )
      {
         EnterCriticalSection(&g_sec);
         m_pTemp->Delete(0);
         LeaveCriticalSection(&g_sec);
      }
     


아래는 코드는 DB에 추가하는 소스의 일부이고요 태스크 쓰레드에서 돌고 있습니다.
DB추가 부분 및 관계없는 소스는 생략하였습니다.
m_pTemp에서 로그 데이터를 DB에 추가하고 나중에 Clear() 한다는 점만 보면됩니다.

 
      //Thread에서 Log를 Add하는 경우를 위해 Critical_section으로 묶어 둔다.
      EnterCriticalSection(&g_sec);
      
      for(int i=0;i< m_pTemp->Count;i++)
      {
           // DB에 데이터 추가
      }
      m_pTemp->Clear();

      LeaveCriticalSection(&g_sec);
     


위 두개의 소스에서는 별개 쓰레드가 m_pTemp 라는 TStringList을 접근하기 때문에 안전하게 하기 위해서 EnterCriticalSection(&g_sec);LeaveCriticalSection(&g_sec); 을 사용하여 서로 동시 접근하지 않도록 막아 준것 처럼 보입니다.

하지만 실제로 위 소스로 프로그램을 돌리다면 중간에 예외(EXCEPTION)가 하나 발생합니다.,
out of bounds 에러이지요


에러가 발생하는 시나리오(타이밍)을 보도록 합시다


일단 태스크 쓰레드가 아래 빨간색 부분을 실행 하고 있다고 가정해 보죠

 
      //Thread에서 Log를 Add하는 경우를 위해 Critical_section으로 묶어 둔다.
      EnterCriticalSection(&g_sec);
      
      for(int i=0;i< m_pTemp->Count;i++)
      {
         <----------NOW 이부분 실행중
           // DB에 데이터 추가
      }
      m_pTemp->Clear();

      LeaveCriticalSection(&g_sec);
     

이때 Main GUI 쓰레드는 다음 소스의 while 조건문을 체크 하는 타이밍이 나올 수 있습니다.

 
      //----------------------------------------------------------------------------
      // Temp Log 데이터도 삭제 함 : String List에서 max count 초과 Log 삭제
      while( m_pTemp->Count > F_OptDlg->E_MaxLogLine->GetInt() )
      {
         EnterCriticalSection(&g_sec);
         m_pTemp->Delete(0);
         LeaveCriticalSection(&g_sec);
      }
     

<----------NOW 이 부분을 실행중일 때는 태스크쓰레드는 EnterCriticalSection
안으로 들어가 있지만.. Min GUI 쓰레드는  EnterCriticalSection 을 실행하지 않아서 while 조건문까지는 진행됩니다.
m_pTemp->Clear();  를 클리어 하기 전이므로 m_pTemp->Count는 0이 아닐 가능성이 있습니다.
이 다음 라인에서 기다리다가 위의 태스크 쓰레드가  m_pTemp->Clear();  를 실행하고 빠져나갈때
GUI 쓰레드는 m_pTemp->Delete(0); 를 실행하게 됩니다.

이때는 물론 m_pTemp->Clear()가 실행되어 m_pTemp에는 아이템이 전혀 없습니다.
따라서 아이템이 없는 StringList를 접그 하려는 m_pTemp->Delete(0); 에서 EXCEPTION 이 발생하게 됩니다.

위코드는 다음과 같이 EnterCriticalSection를 while 조건문 밖으로 빼내어야 멀티쓰레드 동시 접근 문제를 완벽하게 막는 구조가 됩니다.

 
      // Temp Log 데이터도 삭제 함 : String List에서 max count 초과 Log 삭제
      EnterCriticalSection(&g_sec); // CriticalSection을 밖으로 뺌
      while( m_pTemp->Count > F_OptDlg->E_MaxLogLine->GetInt() )
      {
         m_pTemp->Delete(0);
      }
      LeaveCriticalSection(&g_sec);
     

위 문제는 다행히 out of bounds 에러가 자주 발생하기 때문에 금방 발견이 되었지만
실제로 멀티쓰레드에서 발생하는 동시 접근으로 인한 버그는 찾아내기가 굉장히 어렵습니다.

위 내용은 비교적 간단한 시나리오로 멀티 쓰레드에서 발생할 수 있는 버그를 이해하기 쉽게 단적으로 잘 보여주는 예인것 같습니다. 실전에서는 항상 이런 문제에 주의를 집중하여 코드를 작성할  필요가 있습니다.
참고바랍니다.