본문 바로가기
개발, IT

뮤텍스(mutex)와 세마포어(semaphore)란?

by Nabi™ 2008. 5. 30.

뮤텍스(mutex)와 세마포어(semaphore)란?

뮤텍스란 MUTual EXclusion 으로 우리말로 해석하면 '상호 배제'라고 한다.
Critical Section을 가진 Thread들의 running time이 서로 겹치지 않게, 각각 단독으로 실행되게 하는 기술이다.
 * Critical Section : 프로그램 상에서 동시에 실행될 경우 문제을 일으킬 수 있는 부분.

만약 어느 Thread에서 Critical Section을 실행하고 있으면 다른 Thread들은 그 Critical Section에 접근할 수 없고 앞의 Thread 가 Critical Section을 벗어나기를 기다려야 한다.

그리고 세마포어란 역시 데드락을 피하기 위한 기술 중에 하나이다.

Thread 가 Critical Section에 접근할때 (또는 processer로 부터 resource를 할당받을때) 해당 Thread는 세마포어의 카운트를 감소시키고 수행이 종료된 후엔 세마포어의 카운트를 원래대로 증가시킨다.
다시말하면 세마포어는 일종의 신호등의 역할을 하는것이다.
현재 실행중인 Thread가있으면 빨간불을 켜서 다른 Thread가 들어오지 못하도록 하는것이다.
(실제로는 세마포어는 일종의 카운터함수와 같은 형태이다. 다만 카운트와 더불어 flag의 역할까지 한다고 하면 맞을려나..)

UI Thread

Worker Thread와의 차이는 저번에 알아본 바와 같이 Windows Message 펌프가 있는가 없는가의 차이가 있을 뿐이다. 결국 UI쓰래드를 하나 생성한다면 윈도우 메시지처리를 하는 또 하나의 쓰래드를 만드는 것이다.

UI 쓰래드를 만드는 방법은 MFC를 사용하는 방법이 유일한 것 같다. (일반 Win32 API로는 UI쓰래드를 만들기가 힘든 것 같다.) 그 방법은 CWinThread 클래스로부터 UI메시지 처리를 하게될 새로운 객체를 파생시키기만 하면 된다. 그후 AfxBeginThread를 사용하거나 CWinThread 클래스의 CreateThread 멤버 함수를 사용해서 쓰래드를 시작하면 된다.

UI 쓰래드는 성격상 UI쓰래드가 받아온 메시지를 처리할 또다른 윈도우가 필요하다(그 결과를 윈도우에 표시할 필요가 없다면 꼭 필요한 것은 아니다.) 반복되는 얘기겠지만 UI쓰래드가 실행되면 현재의 시스템에서 발생되는 모든 윈도우 메시지들은 그 쓰래드에게 적절하게 자동으로 보내지게된다.

MSVC의 온라인 헬프에서 "multithreaded programming" 이란 항목으로 조사를 하다보면 몇 가지 샘플 프로그램들이 있는데, 거기에서 UI 쓰래드 예제는 단 한가지 밖에 없다. "MTMDI" 라는 것인데, MDI 프로젝트에서 각 View 윈도우마다 각자의 UI쓰래드를 두어 사용하는 것이었다. 그것이 UI쓰래드의 가장 적절한 사용처가 아닐까 싶다.

어떤 경우가 UI 쓰래드를 위한 가장 좋은 예가 될까... 나도 잘 모르겠다... 혹시 아시는 분 있으면 연락 바란다.

Critical Section(임계 섹션)

이전 스터디에서 잠깐 언급했던 글로벌 변수를 이용한 쓰래드간의 정보교환에 의한 버그를 신경쓰지 않고 쓰래드들 간에 글로벌 데이터를 공유하고자할 때 유용하게 쓰이는 방법이 바로 Critical Section을 사용하는 것이다.

* 이벤트는 시그널화에, 임계 섹션은 데이터에 대한 억세스를 제어하는데 적합하다.

만약 쓰래드 A와 B가 시간(Time)값을 공유한다고 가정해 보자, A가 시간(Hour)값을 변경하고 분(Minute)값을 변경하려는 시점에 쓰래드 B가 시간값을 참조하게 된다면(인터럽트) 쓰래드 B가 참조하는 시간(Time)값은 전혀 엉뚱한 것이 될 것이다. 그럼, 다음의 클래스를 살펴보자.

#include "stdafx.h" 

class CHMS
{
    private:
    int m_nHr, m_nMn, m_nSc;
    CRITICAL_SECTION m_cx;

Public:

    CHMS() : m_nHr(0), m_nMn(0), m_nSc(0)
    {
        ::InitializeCriticalSection(&m_cs);
    }
    
    ~CHMS()
    {
        ::DeleteCriticalSection(&m_cs);
    }
    
    void SetTime(int nSecs)
    {
        ::EnterCriticalSection(&m_cs);
        m_nSc = nSecs % 60;
        m_nMn = (nSecs/60) % 60;
        m_nHr = nSecs / 3600;
        ::LeaveCriticalSection(&m_cs);
    }
    
    int GetTotalSecs()
    {
        int nTotalSecs;
        ::EnterCriticalSection(&m_cs);
        nTotalSecs = m_Hr*3600 + m_nMn*60 + m_nSc;
        ::LeaveCriticalSection(&m_cs);
        
        return nTotalSecs;
    }

    void IncrementSecs()
    {
        ::EnterCriticalSection(&m_cs);
        SetTime( GetTotalSecs() + 1 );
        ::LeaveCriticalSection(&m_cs);
    }
};

위의 클래스는 시간값(시,분,초)을 참조/수정 하기위한 데이터와 함수들을 가지고 있다. 이 클래스의 멤버중에 CRITICAL_SECTION 타입의 멤버가 있음을 주목하자. 생성자는 InitializeCriticalSection 함수를 호출하며 소멸자는 DeleteCriticalSection 함수를 호출한다. 그리고 다른 함수들은 EnterCriticalSection 과 LeaveCriticalSection 함수를 호출한다.

*위의 예제는 아마도 역시 MSDN의 부실한 헬프에서 따다온 것일텐데, 그래도 Criticla Section의 다양한 면을 보는데 아주 좋은것 같다.

즉, 위의 코드를 간단히 설명하자면, 어떤 지정된 출입문(Critical section이 바로 그 문이다) 을 통과해야만 어떤 물건(여기서는 시간값이 되겠지...)를 만질 수 있다는 것이다. 이 문은 한번에 한 사람만 들어갔다 나올 수 있다.

어랏, 근데 위의 코드를 보면 IncrementSecs 함수가 SetTime를 호출하는데, 이 두 함수 모두 하나의 임계섹션을 사용하게 된다. 그렇게 되면 문제가 생기지 않을까? 신기하게도 문제가 생기지 않는다...

*이렇게 critical section이 '중첩'되면  Windows가 이 중첩수준을 추적해 줄 것이다.

*힙에서 생성한 객체 포인터들을 공유할 경우는 또 다른 문제들이 발생한다. 각 쓰래드는 다른 쓰래드가 객체를 삭제했는가의 여부를 판단해야만 하고(당연하지요) 그렇게되면 여러분이 억세스를 포인터들의 삭제/생성 시점과 동기화를 시켜줘야만 한다.

만약 MFC 클래스들을 사용한다면 더 프로그램 짜기가 쉬워질 것이다. 나는 CCriticalSection 객체를 하나 만들어 동기화 준비를 해 놓고 CSingleLock 객체를 사용하여 CCriticalSection 객체를 Lock, Unlock 하면서 쉽게 동기화를 구현한다.. 자세한 설명은 MSDN 헬프를 보시길..


MUTEX

Mutex란? (Mutual Exclusion<상호 배타>의 준말) 는 상호간에 asynchronously 하게 작동하는 쓰래드간 또는 프로세스간에 '통신'을 위한 한 방법이다. 이 '통신'은 주로 다수의 쓰래드(또는 프로세스)들의 공유 리소스에 대한 접근을 리소스를 "locking" 과 "unlocking"을 통해 조율하게된다.

특정 x,y 좌표에 점을 찍는 프로그램을 만들 때, A쓰래드가 x,y 좌표를 변경하는 일을 하고 B쓰래드는 그 변경된 좌표값 대로 화면을 점을 찍는 역할을 한다면, 이경우 역시 두 쓰래드간에 동기화가 되어야 정확한 좌표에 점을 찍을 수 있게된다. 그래서 쓰래드 A가 좌표값을 변경하는 경우는 다른 어떠한 쓰래드도 그 값을 참조/변경하지 못하도록 막아놓고(Lock: mutex를 set한다.) 그 작업이 끝나면 잠금을 풀어주어(Unlock: mutex를 clear한다.) 쓰래드 B가 그 좌표값을 읽어와서 화면에 점을 찍을 수 있도록 하는 것이 정석이다. Mutex가 설정되어 있는 경우 다른 쓰래드는 Mutex가 해제될 때까지 기다려야만 한다.

* 그럼 Critical Section과의 차이는 무엇인가? 그 첫째는 Critical Section은 좀더 빠른 처리를 요하는 경우, 그리고 그 공유하고자 하는 리소스가 프로세스와 프로세스사이는 넘을 수 없는 경우(단지 쓰래드 사이에서만 공유하는 경우)에 유용하다.

CriticalSection과의 차이점 그 두번째는? 으하하 사용방법의 차이이다…. (별거아니군) 화면에 점을 찍을 준비가 된 쓰래드는 WaitForSingleObject를 호출하여 Mutex이 해제될 때까지 기다렸다가 mutex을 설정하고 점을 찍게 된다. 점을 다 찍고 나면 다시 mutex를 해제시켜준다. 다음의 예제소스를 참고하길 바란다.

좀더 복잡한 경우를 생각해보자. 만약 어떤 프로그램에서 복수의 쓰래드가 동일한 파일을 사용하는 경우, 다른 쓰래드가 파일 포인터를 엉뚱한 곳으로 이동시켜버렸을 수 있으므로, 각 쓰래드는 읽거나 쓰기 전에 반드시 파일포인터를 재설정 해야만 한다. 추가로, 각 쓰래드는 파일 포인터가 자신이 파일 포인터를 소유하고 파일을 접근하는 사이에 이미 다른 쓰래드에 의해 선점되지 않았는지 확인해야만 한다.

HANDLE hIOMutex= CreateMutex (NULL, FALSE, NULL);
WaitForSingleObject( hIOMutex, INFINITE );
fseek( fp, desired_position, 0L );
fwrite( data, sizeof( data ), 1, fp );
ReleaseMutex( hIOMutex);

다음의 예제를 보도록 하자. MSDN에서 따온 예제이다... 좀 길지만 차근차근 보다 보면 소스코드가 이해가 갈 것이다. 백번 설명하는 것 보단 한번 소스코드를 이해하는 것이 더 낫다.

/* Bounce - Creates a new thread each time the letter 'a' is typed.
* Each thread bounces a happy face of a different color around the screen.
* All threads are terminated when the letter 'Q' is entered.
*
* This program requires the multithread library. For example, compile
* with the following command line:
* CL /MT BOUNCE.C
*/

#include 
#include 
#include 
#include 
#include 
#include 

#define MAX_THREADS 32

/* getrandom returns a random number between min and max, which must be in
* integer range.
*/
#define getrandom( min, max ) ((rand() % (int)(((max) + 1) - (min))) + (min))

void main( void ); /* Thread 1: main */
void KbdFunc( void ); /* Keyboard input, thread dispatch */
void BounceProc( char * MyID ); /* Threads 2 to n: display */
void ClearScreen( void ); /* Screen clear */
void ShutDown( void ); /* Program shutdown */
void WriteTitle( int ThreadNum ); /* Display title bar information */

HANDLE hConsoleOut; /* Handle to the console */
HANDLE hRunMutex; /* "Keep Running" mutex */
HANDLE hScreenMutex; /* "Screen update" mutex */
int ThreadNr; /* Number of threads started */
CONSOLE_SCREEN_BUFFER_INFO csbiInfo; /* Console information */


void main() /* Thread One */
{
    /* Get display screen information & clear the screen.*/
    hConsoleOut = GetStdHandle( STD_OUTPUT_HANDLE );
    GetConsoleScreenBufferInfo( hConsoleOut, &csbiInfo );
    ClearScreen();
    WriteTitle( 0 );
    /* Create the mutexes and reset thread count. */
    hScreenMutex = CreateMutex( NULL, FALSE, NULL ); /* Cleared */
    hRunMutex = CreateMutex( NULL, TRUE, NULL ); /* Set */
    ThreadNr = 0;

    /* Start waiting for keyboard input to dispatch threads or exit. */
    KbdFunc();

    /* All threads done. Clean up handles. */
    CloseHandle( hScreenMutex );
    CloseHandle( hRunMutex );
    CloseHandle( hConsoleOut );
}

void ShutDown( void ) /* Shut down threads */
{
    while ( ThreadNr > 0 )
    {
        /* Tell thread to die and record its death. */
        ReleaseMutex( hRunMutex );
        ThreadNr--; 
    }
    /* Clean up display when done */
    WaitForSingleObject( hScreenMutex, INFINITE );
    ClearScreen();
}

void KbdFunc( void ) /* Dispatch and count threads. */
{
    int KeyInfo;

    do
    {
        KeyInfo = _getch();
        if( tolower( KeyInfo ) == 'a' && ThreadNr < MAX_THREADS )
        {
            ThreadNr++;
            _beginthread( BounceProc, 0, &ThreadNr );
            WriteTitle( ThreadNr );
        }
    } while( tolower( KeyInfo ) != 'q' );

    ShutDown();
}

void BounceProc( char *MyID )
{
    char MyCell, OldCell;
    WORD MyAttrib, OldAttrib;
    char BlankCell = 0x20;
    COORD Coords, Delta;
    COORD Old = {0,0};
    DWORD Dummy;

    /* Generate update increments and initial display coordinates. */
    srand( (unsigned) *MyID * 3 );
    Coords.X = getrandom( 0, csbiInfo.dwSize.X - 1 );
    Coords.Y = getrandom( 0, csbiInfo.dwSize.Y - 1 );
    Delta.X = getrandom( -3, 3 );
    Delta.Y = getrandom( -3, 3 );

    /* Set up "happy face" & generate color attribute from thread number.*/
    if( *MyID > 16)
        MyCell = 0x01; /* outline face */
    else
        MyCell = 0x02; /* solid face */
    
    MyAttrib = *MyID & 0x0F; /* force black background */

    do
    {
        /* Wait for display to be available, then lock it. */
        WaitForSingleObject( hScreenMutex, INFINITE );

        /* If we still occupy the old screen position, blank it out. */
        ReadConsoleOutputCharacter( hConsoleOut, &OldCell, 1, Old, &Dummy );
        ReadConsoleOutputAttribute( hConsoleOut, &OldAttrib, 1, Old, &Dummy );
        if (( OldCell == MyCell ) && (OldAttrib == MyAttrib))
            WriteConsoleOutputCharacter( hConsoleOut, &BlankCell, 1, Old, &Dummy );

        /* Draw new face, then clear screen lock */
        WriteConsoleOutputCharacter( hConsoleOut, &MyCell, 1, Coords, &Dummy );
        WriteConsoleOutputAttribute( hConsoleOut, &MyAttrib, 1, Coords, &Dummy );
        ReleaseMutex( hScreenMutex );

        /* Increment the coordinates for next placement of the block. */
        Old.X = Coords.X;
        Old.Y = Coords.Y;
        Coords.X += Delta.X;
        Coords.Y += Delta.Y;

        /* If we are about to go off the screen, reverse direction */
        if( Coords.X < 0 || Coords.X >= csbiInfo.dwSize.X )
        {
            Delta.X = -Delta.X;
            Beep( 400, 50 );
        }
        if( Coords.Y < 0 || Coords.Y > csbiInfo.dwSize.Y )
        {
            Delta.Y = -Delta.Y;
            Beep( 600, 50 );
        }
    }
    
    /* Repeat while RunMutex is still taken. */
    while ( WaitForSingleObject( hRunMutex, 75L ) == WAIT_TIMEOUT );
}

void WriteTitle( int ThreadNum )
{
    char NThreadMsg[80];

    sprintf( NThreadMsg, "Threads running: %02d. Press 'A' to start a thread,'Q' to quit.", ThreadNum );
    SetConsoleTitle( NThreadMsg );
}

void ClearScreen( void )
{
    DWORD dummy;
    COORD Home = { 0, 0 };
    FillConsoleOutputCharacter( hConsoleOut, ' ', csbiInfo.dwSize.X * csbiInfo.dwSize.Y, Home, &dummy );
}

Semaphore

세마포어는 한정된 수의 사용자만을 지원할 수 있는 공유 자원에 대한 접근을 통제하는데 유용하다. MFC에서는 지금까지 알아본 Critical Section, Mutex, Semaphore에 대한 클래스를 제공해 준다. 이 클래스들은 모두 CSyncObject에서 파생되었으며, 따라서 사용법도 비슷하다.

다음의 내용은 어느 경우에 어떤 방식을 사용할 것인지에 대한 것으로, MS VC++ 5.0의 Online help에 "Multithreading: When to Use the Synchronization Classes" 라는 제목으로 개제되어 있는 내용중의 일부이다.

To determine which synchronization class you should use, ask the following series of questions:

1. 프로그램이 자원에 접근하기 전에 어떤 일이 발생할 때까지 기다려야만 합니까?
그렇다면 CEvent를 사용하십시오.

2. 동일한 프로그램내에 존재하는 하나 이상의 쓰래드가 동시에 같은 리소스를 접근해야 합니까? (예를 들어 하나의 다큐먼트에 대해 다섯개의 뷰를 제공하려고 할 때)
그렇다면 CSemaphore.를 사용하십시오.

3. 하나 이상의 프로그램이 특정 리소스를 사용하도록 하겠습니까?(예를 들어 DLL에 들어 있는 리소스)
그렇다면 CMutex.를 사용하시고
아니라면 CCriticalSection. 을 사용하십시오.

좋은정보가 되셨다면 아래 한번 클릭해주세요^^



댓글