쓰레드 동기화 오브젝트

 (Thread Synchronization Objects)

 쓰레드가 2개 이상 실행될 때 여러 가지 변수가 있습니다.

하나의 공유자원(예를 들어 동시에 접근하는 변수) 에 접근할 때, 파일 입출력 이나 디바이스I/O작업을 할 때 동기화 오브젝트가 필요합니다.

동기화 오브젝트 없이 쓰레드가 공유 자원을 사용할 때 공유자원이 원치 않은 값이 될수 있고, I/O작업 시 쓰레드가 I/O작업이 끝날 때 까지 무한정 블로킹(blocking : 특정 함수가 리턴 될 때 까지 기다림)현상이 발생할 수 있습니다.

동기화 오브젝트를 사용하여 다중 쓰레드에서 어떻게 안전 하게 공유자원에 접근하고 다른 쓰레드간의 실행 순서등을 조작하는지에 대해 알아 보겠습니다.

동기화 오브젝트는 유저 모드와 커널 모드로 분류할 수 있습니다.

  유저 모드

 유저 모드는 현재 프로세스/쓰레드 내의 상태를 말합니다.

유저모드에서는 커널 오브젝트(프로세스, 파일, 디바이스 등)으로 바로 접근을 할 수 없고, 커널 오브젝트로 접근 시 시스템에 의해 변환 작업이 이루어집니다.

이런 변환 작업은 시간을 많이 걸리는 작업이기 때문에 유저 모드가 커널 모드 보다 속도가 빠릅니다.

유저 모드 동기화 방법은 코드레벨에서 동기화 하는 방법을 이야기 합니다.

유저모드에서 동기화는 사용하기 쉽고 커널 모드 동기화 함수들에 비해 속도가 빠른 장점이 있지만 커널 오브젝트(파일 I/O, 프로세스, 쓰레드 등)의 동기화는 불가능하다는 단점이 있습니다.

 Interlocked

 Interlocked 함수들은 다중 쓰레드에서 공유변수들을 안전하게 1씩 증가/감소, 특정값을 증가 , 비트 연산을 할 수 있습니다.

Intlocked 함수는 사용하기 쉬우므로 길게 설명은 하지 않겠습니다.

아래 사이트를 참조하시길 바랍니다.

http://msdn2.microsoft.com/en-us/library/ms686360(VS.85).aspx

 

 크리티컬 섹션(CRITICAL SECTION)

 크리티컬 섹션은 특정 코드영역을 쓰레드가 동시에 실행되는 것을 막아 줍니다.

아래는 크리티컬 섹션 관련 함수들입니다.

 

DeleteCriticalSection

크리티컬 섹션 오브젝트를 삭제 합니다.

InitializeCriticalSection

크리티컬 섹션 오브젝트를 초기화 합니다.

InitializeCriticalSectionAndSpinCount

크리티컬 섹션 오브젝트를 초기화 하고 스핀 카운트를 설정합니다.

InitializeCriticalSectionEx

크리티컬 섹션을 초기화하고 스핀카운트 설정, 부가기능을 설정합니다.

LeaveCriticalSection

크리티컬 섹션 오브젝트 권한을 해제합니다..

SetCriticalSectionSpinCount

특정 크리티컬 섹션 오브젝트 스핀카운트를 설정합니다.

TryEnterCriticalSection

블로킹(Wait) 되지 않고 크리티컬 섹션 오브젝트의 권한을 요청합니다.


InitializeCriticalSectionAndSpinCount 함수는 스핀 카운트를 두어서 쓰레드가 크리티컬 섹션 오브젝트를 획득하지 못하면 Wait상태로 일정 시간(스핀카운트) 루프를 돌아 크리티컬 섹션오브젝트가 해제 되었는지 체크합니다.

해제되지 않았으면 Sleep하게 됩니다.

이 함수는 멀티 프로세서 환경에서만 유효하며, 스핀카운터는 4000을 추천(Windows Via C/C++)하지만 자신의 환경에서 값을 바꾸어 가며 테스트 해보길 권장합니다.

         

 #include <Windows.h>

CRITICAL_SECTION g_cs;

 //크리티컬 섹션 오브젝트를 초기화 합니다.

void InitCriticalSection()

{

        InitializeCriticalSection(&g_cs);

}

 

unsigned _stdcall CallThreadHandlerProc(void *pThreadHandler)

{

        while (bExit == FALSE)

        {

               if(TryEnterCriticalSection(&g_cs))

               {

                       //쓰레드 작업을 수행 합니다.

                       //수행 하고 LeaveCriticalSection 함수를 호출

                       //하여 크리티컬 섹션 오브젝트를 해제합니다.

 

                       LeaveCriticalSection(&g_cs);

               }

               else

               {

                        // 크리티컬 섹션 오브젝트 획득에 실패 시 수행할

                        // 작업을 선언합니다.

                        // SwitchToThread함수를 호출하여 다른 쓰레드로

                        // 스위칭 합니다.

                       SwitchToThread();                    

               }             

        }

        DWORD exitCode;

        GetExitCodeThread(InputThrd, &exitCode);

        _endthreadex(exitCode);

        return 0;

}


**Sleep() 함수와 SwitchToThread()함수는 디스패쳐가 다른 쓰레드로 스케쥴 하도록 합니다.

차이점은 Sleep()함수는 현재 쓰레드보다 우선순위가 같거나 높은 쓰레드가 없으면 쓰레드 전체를 리스케쥴링(rescheduling)합니다.

 

Slim Reader/Writer Locks

 Slim reader/writer (SRW) locks 하나의 프로세스내의 쓰레드들이 공유자원을 동기화 할 수 있습니다.

아주 작은 메모리를 차지하면서 속도도 빠릅니다.

Reader 쓰레드는 공유자원을 읽고 Writer 쓰레드는 공유자원에 쓰기 작업을 할수 있습니다.

다중 쓰레드가 공유자원을 읽고 쓰기를 , 크리티컬 섹션과 뮤텍스 같은 상호배제 오브젝트(exclusive locks)들은 reader 쓰레드는 계속 돌고 writer 쓰레드는 거의 돌지 못하면 병목현상(bottle neck) 발생 있습니다.

SRW locks 공유자원에 접근 할 수 있는 두 가지 모드를 제공합니다.

·Shared mode : 읽는 작업을 하는 쓰레드가 여러 개일 때 공유자원을 읽기 전용으로 접근할 수 있도록 해서 동시 다발적으로 작업을 할 수 있도록 합니다.  만약 읽는 작업이 쓰는 작업을 초과 할 경우, 성능과 처리량은 크리티컬 섹션과 동일하게 됩니다.
·Exclusive mode : 읽기/쓰기 쓰레드는 하나의 쓰레드만 접근 할 수 있습니다.  Exclusive mode로 락이 걸려지면 다른 쓰레드들은 공유자원에 접근할 수 없습니다.

하나의 SRW lock 두 가지 모드를 동시에 가질 수 있습니다. 읽는 쓰레드는 Shared mode 쓰는 쓰레드는 Exclusive mode 작업을 있습니다.  어떤 쓰레드가 소유권을 먼저 가질지는 알수 없습니다. SRW Locks 공정하거나 선입선출(First In First Out : FIFO)방식이 아닙니다.

SRW lock 포인터 크기를 가집니다. 장점은 속도가 빠르고 lock상태의 변환이 빠르다는 입니다.

단점은 아주 작은 상태 정보만 저장이 되어 재귀적으로 SRW locks 가질 없습니다. Shared Mode 쓰레드가 Shred Mode 변환 될수 없습니다.

SWR Locks Windows Server 2008,Vista 에서만 사용이 가능합니다.

아래는 SRW lock 함수들 입니다.

SRW lock function

Description

AcquireSRWLockExclusive

SRW lock  exclusive mode 얻습니다.

AcquireSRWLockShared

SRW lock shared mode 얻습니다.

InitializeSRWLock

SRW lock 초기화 합니다.

ReleaseSRWLockExclusive

exclusive mode SRW lock 해제 합니다.

ReleaseSRWLockShared

shared mode SRW lock 해제 합니다.

SleepConditionVariableSRW

SRW작업이 완료 될때까지 Sleep 합니다.

 커널 모드

유저 모드에서 커널 오브젝트에 접근을 할 때 시스템은 커널 모드로 변환을 합니다.

커널 오브젝트에는 File, Event, Mutex, Semaphore, Process, Waitable Timer, Job, Thread 가 있습니다.

커널 모드에서 동기화는 커널 오브젝트가 non-Signal인지 Signal 상태인지를 보고 쓰레드를 스케쥴링합니다.

커널 오브젝트가 signal상태이면 쓰레드가 돌아갈 준비가 된 상태이고, non-signal 상태이면 쓰레드는 기다림(Wait)상태 입니다.

 

이벤트(EVENT)
 가장 많이 보편화되고 많이 쓰는 다중 쓰레드 동기화 오브젝트가 이벤트가 아닌가 생각이 됩니다.

이벤트로 다중 쓰레드 동기화 하는 방법에는 두 가지 방법이 있습니다.

개발자가 직접 이벤트를 수동으로 signal/non-signal 상태로 변환 하는 것과 시스템이 자동으로 이벤트를 signal/non-signal상태로 변환 하는 방법입니다.

이벤트를 기다리는 방법은 WaitForSingleObjec/WaitForSingleObject 함수를 사용합니다. 이벤트가 non-signal상태가 될 때까지 쓰레드는 Wait상태로 됩니다.

이벤트 및 모든 커널 오브젝트는 사용이 끝나면 CloseHandle로 사용을 종료 해야 합니다.

 

Event function

Description





CreateEvent

이벤트 오브젝트를 생성하거나 오픈 합니다.





CreateEventEx

이벤트 오브젝트를 생성하거나 오픈 합니다.(접근 권한을 줄수 있습니다.)





OpenEvent

존재하는 이름이 있는 이벤트 오브젝트를 오픈 합니다.





PulseEvent

특정 이벤트 오브젝트를 signal 상태로 바꾸고 일정시간 non-signal상태로 바끕니다.





ResetEvent

이벤트 오브젝트를 non-signal 상태로 놓습니다.        





SetEvent

이벤트 오브젝트를 signal 상태로 놓습니다.          






 
간단 하게 SDI 프로그램에서 수동 모드(passive mode) 이벤트를 이용하여 사각형과 원을 그리는 멀트 쓰레드 프로그램을 보겠습니다.

 BOOL CEventSampleView::PreCreateWindow(CREATESTRUCT& cs)

{

        // TODO: CREATESTRUCT cs를 수정하여 여기에서

        //  Window 클래스 또는 스타일을 수정합니다.

        srand( (unsigned)time( NULL ) );

 

        //종료조건을 초기화합니다.

        m_bContinue = TRUE;

        //수동모드 이벤트 오브젝트를 생성합니다.

        m_DrawEvent = CreateEvent(0, TRUE, TRUE, _T("DrawEvent"));

 

        // 시그널 상태로 둡니다.

        SetEvent(m_DrawEvent);

       

        return CView::PreCreateWindow(cs);

}

 

/*OnDraw 메시지 핸들러에서 사각형을 그리는 쓰레드와

원을 그리는 쓰레드를 생성합니다.*/

void CEventSampleView::OnDraw(CDC* /*pDC*/)

{

        CEventSampleDoc* pDoc = GetDocument();

        ASSERT_VALID(pDoc);

        if (!pDoc)

               return;

        m_RectThrd = AfxBeginThread(RectThreadProc,reinterpret_cast<LPVOID>(this));

        m_CricleThrd = AfxBeginThread(CircleThreadProc, reinterpret_cast<LPVOID>(this));

        // TODO: 여기에 원시 데이터에 대한 그리기 코드를 추가합니다.

}

 

/*사각형을 그리는 쓰레드 프로시저*/

UINT CEventSampleView::RectThreadProc(__in LPVOID lpParameter)

{

        CEventSampleView* pView = reinterpret_cast<CEventSampleView*>(lpParameter);

        int x=0,y=0, cx=0, cy=0;

        cx = 100;

        cy = 100;

        while(pView->m_bContinue)

        {

               //시그널상태가 될 때까지 기다립니다.

               WaitForSingleObject(pView->m_DrawEvent, INFINITE);

               //이벤트 오브젝트를 받아오면 넌시그널 상태로 둡니다.

               ResetEvent(pView->m_DrawEvent);

              

               HDC hDC = ::GetDC(pView->GetSafeHwnd());

               x= rand()%300;

               y = rand()%300;

               ::Rectangle(hDC,x, y, x+cx, y+cy);

               ::ReleaseDC(pView->GetSafeHwnd(),hDC);

               // 작업이 끝나면 이벤트를 시그널상태로 두어 다음 쓰레드가 가질 수

               // 있도록 합니다.

               SetEvent(pView->m_DrawEvent);

        }

        DWORD dCode=0;

        GetExitCodeThread(pView->m_CricleThrd->m_hThread, &dCode);

        AfxEndThread(dCode, TRUE);

        return 0;

}

/*원을 그리는 쓰레드 프로시저*/

UINT CEventSampleView::CircleThreadProc(__in LPVOID lpParameter)

{

        CEventSampleView* pView = reinterpret_cast<CEventSampleView*>(lpParameter);

        int x=0,y=0, cx=0, cy=0;

        cx = 100;

        cy = 100;

        while(pView->m_bContinue)

        {

               WaitForSingleObject(pView->m_DrawEvent, INFINITE);

               ResetEvent(pView->m_DrawEvent);

               HDC hDC = ::GetDC(pView->GetSafeHwnd());

               x= rand()%300;

               y = rand()%300;

               ::Ellipse(hDC,x, y, x+cx, y+cy);

               ::ReleaseDC(pView->GetSafeHwnd(),hDC);

               SetEvent(pView->m_DrawEvent);

        }

        DWORD dCode=0;

        GetExitCodeThread(pView->m_CricleThrd->m_hThread, &dCode);

        AfxEndThread(dCode, TRUE);

        return 0;

}

//프로그램이 종료할 때 종료조건을 맞춰 주고 쓰레드 종료 메시지를 주어 정상적으로

//종료하도록 하고, 이벤트 핸들을 닫습니다.

void CEventSampleView::OnDestroy()

{

        CView::OnDestroy();

        m_bContinue = FALSE;

        DWORD dExitCode = 0;

        GetExitCodeThread(m_CricleThrd->m_hThread,&dExitCode);

        PostQuitMessage(dExitCode);

 

        GetExitCodeThread(m_RectThrd->m_hThread,&dExitCode);

        PostQuitMessage(dExitCode);

 

        CloseHandle(m_DrawEvent);

}


 
뮤텍스(MUTEX)

뮤텍스는 하나의 공유자원에 대한 상호 배타(mutual exclusive)적으로 동기화 하는 방법입니다.  뮤텍스는 서로 다른 프로세스의 쓰레드의 동기화를 할수 있습니다.  이 특성을 이용해서 보통 하나 이상의 프로그램을 실행하기 위해 뮤텍스를 이용합니다.(이걸 깨는 방법도 있죠.)

뮤텍스는 다음과 같은 규칙이 있습니다.

Ø  쓰레드의 ID 0(유효하지 않은 쓰레드 ID)이면 뮤텍스의 소유권은 어느 쓰레드에게도 없다는 의미 이고 뮤텍스 오브젝트는 시그널된 상태입니다.

Ø  쓰레드 ID 0이 아닌 값이면, 해당쓰레드(생성 시킨 쓰레드)가 소유권을 가지면 뮤텍스 오브젝트는 non-signal상태 입니다.

Ø  다른 커널 오브젝트와는 달리 뮤텍스는 소유권(thread ownership)이라는 개념이 있습니다. 

뮤텍스를 해제할 때(ReleaseMutex), 쓰레드 ID와 생성할 때 설정한 쓰레드의 ID가 맞지 않으면 해제에 실패하고 시스템은 해당 뮤텍스의 시그널 상태를 기다리는 다른 쓰레드를 스케쥴링 합니다.

뮤텍스의 소유권을 가진 쓰레드가 뮤텍스를 해제 하지 않고 종료 되면, 시스템은 해당 뮤텍스를 “abandoned”상태로 두고, 이 뮤텍스를 기다리는 쓰레드를 찾아 기다리고 있는 쓰레드에 뮤텍스의 소유권을 주고 해당 쓰레드를 스케쥴링 합니다.

 

Mutex function

Description

CreateMutex

뮤텍스 오브젝트를 생성하거나 오픈 합니다.

CreateMutexEx

뮤텍스 오브젝트를 생성하거나 오픈합니다. 접근 권한 속성을 있습니다.

OpenMutex

이름이 있는 뮤텍스 오브젝트를 오픈 합니다.

ReleaseMutex

뮤텍스 오브젝트를 해제합니다.

 http://msdn2.microsoft.com/en-us/library/ms686927(VS.85).aspx

 
세마포어(SEMAPHORE)

 세마포어는 공유 자원의 카운팅의 용도로 사용합니다.

세마포어는 사용 개수(usage count)이외에 signed 32비트 값 2개를 더 가지고 있습니다.

Ø  최대 리소스 카운트(maximum resource count) : 세마포어가 관리할 수 있는 최대 리소스의 개수.

Ø  현재 리소스 카운트(current resource count) : 현재 사용 가능한 리소스의 개수.

세마포어는 다음과 같은 규칙을 가지고 동작을 합니다.

Ø  현재 리소스 카운터가 0보다 크면(>0) 세마포어 오브젝트는 signal 상태입니다.

Ø  현재 리소스 카운터가 0이면, 세마포어 오브젝트는 non-signal상태입니다. 

Ø  시스템은 현재 리소스카운터를 값이 되지 않도록 합니다.

Ø  현재 리소스 카운터는 최대 리소스 카운터 보다 클 수 없습니다.

 

Semaphore function

Description

CreateSemaphore

세마포어를 생성/오픈 합니다.

CreateSemaphoreEx

세마포어를 생성/오픈 합니다. 접근 권한을 있습니다.

OpenSemaphore

이름이 있는 세마포어 오브젝트를 오픈합니다.

ReleaseSemaphore

사용 가능한 리소스 개수를 증가 시킵니다..

 세마포어 사용 예는 아래 사이트를 참조 하세요.

http://msdn2.microsoft.com/en-us/library/ms686946(VS.85).aspx


Waitable Timer

Waitable Timer 오브젝트는 특정 시간이 되면 오브젝트가 시드널 됩니다.

특정 시간 마다 어떤 동작을 해야 할 때 사용할 수 있습니다.

Waitable-timer function

Description

CancelWaitableTimer

Waitable Timer 오브젝트를 비활성화 시킵니다.

CreateWaitableTimer

Waitable Timer 생성하거나 오픈 합니다.

CreateWaitableTimerEx

Waitable Timer 생성/오픈 합니다.

OpenWaitableTimer

이름이 붙여진 Waitable Timer 오브젝트를 오픈 합니다.

SetWaitableTimer

Waitable Timer 오브젝트를 활성화 시키거나, Waitable Timer 오브젝트가 시그널 되었을 완료 통보 프로시져를 등록 있습니다..

TimerAPCProc

SetWaitableTimer 함수로 등로한 완료 통보 프로시져 선언.

 

Timer-queue Timer

 Timer queue Timer 오브젝트는 일정시간이 지나면 시그널 되는 동작은 Waitable Timer 와 같습니다.

Timer Queue Timer 오브젝트는 일정 시간이 지나면 시그널 되는 오브젝트이고, Timer Queue오브젝트가 Timer Queue Timer 오브젝트를 큐 형태로 관리하면서

해당 프로시저를 호출 합니다.

 

위의 그림은 Timer Queue오브젝트가 Timer Queue Timer 오브젝트를 관리하고,

Timer Queue Timer가 시그널 되면 해당 프로시저를 호출하는 모습입니다.


Timer-queue timer function

Description

ChangeTimerQueueTimer

Timer queue timer 오브젝트의 속성을 변경 합니다..

CreateTimerQueue

Timer Queue 오브젝트를 생성합니다.

CreateTimerQueueTimer

Timer Queue Timer 오브젝트를 생성합니다.

DeleteTimerQueue

타이머 오브젝트를 삭제 합니다.

DeleteTimerQueueEx

타이머 오브젝트를 삭제 합니다.

DeleteTimerQueueTimer

Timer Queue 있는 Timer Queue Timer 오브젝트를 삭제 합니다.

 

쓰레드간 통신

  마지막으로 쓰레드간 통신 하는 방법을 알아 보겠습니다.

윈도우 프로그래밍을 하면 윈도우에 SendMessage/PostMessage 함수로 메시지를 보내 듯이 쓰레드에 메시지를 보내고 받으면서 쓰레드간 통신을 할 수 있습니다.

쓰레드에 메시지를 보내는 함수는 PostThreadMessage 입니다.

BOOL PostThreadMessage(      
    DWORD idThread,  
/*해당 쓰레드 ID*/

    UINT Msg,       /*메시지 ID*/

    WPARAM wParam,    /*메시지를 받는 쓰레드로 넘겨주는 WPARAM 인자*/

    LPARAM lParam     /* 메시지를 받는 쓰레드로 넘겨주는 LPARAM 인자*/

);

 
PostThread
로 보낸 메시지는 쓰레드 프로시저에서 PeekMessage/GetMessage로 받을 수 있습니다.

http://msdn2.microsoft.com/en-us/library/ms644936(VS.85).aspx

BOOL GetMessage(      
    LPMSG lpMsg,          
//MSG 구조체의 포인터 타입

    HWND hWnd,             //윈도우 핸들

    UINT wMsgFilterMin,    //필터링할 최소 메시지 ID

    UINT wMsgFilterMax     //필터링할 최대 메시지 ID

);

 

http://msdn2.microsoft.com/en-us/library/ms644943.aspx

 

BOOL PeekMessage(      
    LPMSG lpMsg,          
//MSG 구조체의 포인터 타입

    HWND hWnd,             //윈도우 핸들

    UINT wMsgFilterMin,    //필터링할 최소 메시지 ID

    UINT wMsgFilterMax,    //필터링할 최대 메시지 ID

    UINT wRemoveMsg        //메시지 큐에 해당 메시지를 지울지 안지울지 설정

);

 
PeekMessage
GetMessage의 차이점은, GetMessage는 메시지가 메시지 큐에 들어 올 때가지 블록 되고 Peek메시지는 메시지가 없으면 FALSE를 리턴 합니다.

더 자세한 사항은 차고 사이트를 참고 하세요.

 아래 예제 코드는 쓰레드 프로시져에서 메시지를 확인하고, 다른 작업을 수행 하는 예제 코드입니다.

 unsigned _stdcall CallThreadHandlerProc(void *pThreadHandler)

{

        while (bExit == FALSE)

        {

               MSG msg;

             BOOL res = PeekMessage(&msg, NULL, 0, 0, PM_REMOVE);

             if (res)

             {

                        //0x404메세지 가 들어오면 특정 작업 수행

                    if (msg.message = 0x404)

                    {                  

                           std::cout<<"Message Received"<<std::endl;

                    }

             }

 

               //다른 작업 수행

        }

        DWORD exitCode;

        GetExitCodeThread(InputThrd, &exitCode);

        _endthreadex(exitCode);

        return 0;

}

………………………………………….

//다른 쓰레드에서 쓰레드에 메시지를 보냅니다.

BOOL res = PostThreadMessage(ID1, 0x00404, 0,0 );

 
참고 사이트 및 서적

Windows Via C/C++

http://msdn2.microsoft.com/en-us/library/aa904937(VS.85).aspx

http://windows-programming.suite101.com/article.cfm/win32_message_processing_primer


*출처 :

http://www.devpia.com/MAEUL/Contents/Detail.aspx?BoardID=51&MAEULNo=20&no=8026&ref=8026

+ Recent posts