프로그래밍

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

ictlab 2009. 10. 21. 20:47

멀티 쓰레드 프로그래밍을 할때는 동작중인 모든 쓰레드가 동시에 돌아가고 있기 때문에
같은 데이터를 접근하는 쓰레드들은 철저하게 주의를 기울여야 합니다.
보통 같은 데이터 , 메모리 접근을 차단하기위해 싱크 오브젝트들을 사용하는데는
다음은 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 에러가 자주 발생하기 때문에 금방 발견이 되었지만
실제로 멀티쓰레드에서 발생하는 동시 접근으로 인한 버그는 찾아내기가 굉장히 어렵습니다.

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