본문 바로가기
C/[서적] 뇌를 자극하는 윈도우즈 시프

Chapter 13. 쓰레드 동기화 기법1 - 임계 영역 접근 동기화

by GameStudy 2022. 2. 9.

13.1 쓰레드 동기화란 무엇인가

 

Prologue) 13장 outline

  1. 둘 이상의 쓰레드가 같은 메모리를 접근하는 코드 블럭을 임계 영역이라 함.

    그래서 한 순간에 하나의 쓰레드만 임계 영역에 진입할 수 있도록 동기화가 필요함.

  2. 이때의 동기화를 정확하게는 메모리 접근 동기화라 함. 

    또한 메모리 접근 동기화에는 유저 모드 메모리 접근 동기화와 

    커널 모드 메모리 접근 동기화가 있음.

  3. 유저모드를 통한 메모리 접근 동기화: 커널 모드의 도움을 받지 않고 

    유저가 만든 라이브러리를 통해 메모리 접근 동기화->속도가빠름. 기능은 적음

    커널 모드를 통한 메모리 접근 동기화: 커널 레벨에서 직접 제공해주는 기능으로

    메모리 접근 동기화하는 기법->속도는느림. 기능은많음.

    기능이 많다? 하나의 프로세스 내의 쓰레드 간의 메모리 접근 동기화 뿐만 아니라,

    다른 프로세스 내의 쓰레드끼리의 메모리 접근 동기화도 가능하다!

  4. 우리가 쓰레드를 실행하고 난 후에는 흐름을 컨트롤 할 수 없음. 엎지른 물임.

    그러나, A 쓰레드가 B 쓰레드보다 먼저 실행되었으면 좋겠다 싶을 수 있음.

    즉, A 쓰레드가 데이터를 입력 받아 정제한 데이터를 가지고 B 쓰레드가 연산하는 상황.

    즉 이렇게 순서를 동기화 하는 것도 쓰레드 순서 동기화라 함. 이는 14장 내용.

    

 

13.1-1 메모리 접근 동기화와 실행 순서의 동기화

  Note) 사람 A와 B가 동시에 화장실에 들어가면 둘다 어쩔줄 모름.

 

  Note) 사람 A는 큰게 마렵고, 사람 B는 작은게 마려움.

    그럼 사람 B가 먼저 들어갔다가 금방 나오면 사람 A가 들어가는게 작업 시간에 따르면 맞음.

    근데 실행 순서 동기화를 하게 되면 사람 B가 먼저 들어가야한다면 꼭 그래야 함.

 

 

 

13.1-2 쓰레드 동기화 기법의 두 가지 구분

  Note) 유저 모드 동기화

    - 크리티컬 섹션 기반 동기화

      "크리티컬 섹션 == 임계 영역"은 아님.. 그냥 MS사에서 제공해주는 

      메모리 접근 동기화 기법이라고만 생각하자.

    - 인터락 함수 기반 동기화

      보편적으로 동기화 시킬 영역이 작음. 경우에 따라 1~3개정도의 변수

      이렇게 작은 경우에 인터락 함수 기반 동기화를 사용하면 구현이 수월.

      성능면에선 고려해봐야함.

 

  Note) 커널 모드 동기화

    - 뮤텍스 기반 동기화             : 메모리 접근 동기화

    - 세마포어 기반 동기화          : 메모리 접근 동기화

    - 이름있는 뮤텍스 기반 동기화: 메모리 접근 동기화

    - 이벤트 기반 동기화            : 실행 순서 동기화(14장 내용)

 

 

 

13.2 임계 영역 접근 동기화와 유저 모드 동기화

 

13.2-1 크리티컬 섹션 기반의 동기화

  Note) 동기화 기법은 그냥 제공되는 거임. 내부적 동작은 하드웨어쪽에 가까워짐.

    그냥 올바른 사용방법을 따라하는게 좀 더 중요하다. 내부적 동작은 가볍게만 나중에 설명함.

 

  Note) 모든 동기화 기법은 화장실 키라고 생각하면 됨. 키를 가지고 있으면 접근을 허용함.

CRITICAL_SECTION gCriticalSection;           // critical section object. -> 열쇠 생성.
InitializeCriticalSection(gCriticalSection); // 크리티컬 섹션 기반 동기화를 내부적으로 처리 하기 위한 
                                             // 최소한의 기본작업을 요청하는 함수. 내부적으로 무슨일 하는지는 중허지 않음.

// ... 중략 ...

// 임계 영역 진입을 위한 크리티컬 섹션 오브젝트 획득. 즉 열쇠 획득.
// 여러 쓰레드 중, 빠른 한 명만 열쇠를 얻고 나머지는 Blocked 상태로 빠지게됨.
EnterCriticalSection(&gCriticalSection);     

// ... 임계 영역 ...

// 크리티컬 섹션 오브젝트 반환. 즉 열쇠 반환.
LeaveCriticalSection(&gCriticalSection);

// ... 중략 ...

// 열쇠 소멸.
DeleteCriticalSection(&gCriticalSection);

 

  Note) 안정적으로 하고 싶어서, 무작정 임계영역을 넓게 잡아도될까?

    넓게 잡으면 안정적임. 아리송한 부분이 있다면 다 묶어버리면 안정적이긴 한데,

    해당 쓰레드가 아리송한 부분(사실은 임계영역이 아닌)을 지나갈 때까지

    다른 쓰레드는 대기해야함. 근데 만약 아리송한 부분에 I/O 작업까지 껴져 있다면? 완전 최악.

    따라서 임계 영역을 구성할때는 얼마나 최소화 시키냐가 중요해짐.

    즉, 철저하게 조사해서 필요 없는 부분은 빼버리고 최소한으로만 가져가야 함.

 

  Ex)

<hide/>

/*
    CriticalSectionSync.cpp
    프로그램 설명: 생성 가능한 쓰레드의 개수 측정.
*/

#include <stdio.h>
#include <windows.h>
#include <process.h>
#include <tchar.h>

#define NUM_OF_GATE		6

LONG gTotalCount = 0;

CRITICAL_SECTION   hCriticalSection;


void IncreaseCount()
{
    EnterCriticalSection (&hCriticalSection);
    gTotalCount++; // 대부분 임계 영역이 이런식으로 한두줄임. 그래서 좀 불합리해 보일 수 있음.
                   // why? 크리티컬 섹션 개체의 선언->초기화->획득->반환->소멸의 과정이 너무 길어서.
    LeaveCriticalSection (&hCriticalSection);
}


unsigned int WINAPI ThreadProc( LPVOID lpParam ) 
{ 
    for(DWORD i=0; i<1000; i++)
    {
        IncreaseCount();
    }

    return 0;
} 


int _tmain(int argc, TCHAR* argv[])
{
    DWORD dwThreadId[NUM_OF_GATE];
    HANDLE hThread[NUM_OF_GATE];

    InitializeCriticalSection(&hCriticalSection);

    for(DWORD i=0; i<NUM_OF_GATE; i++)
    {
        hThread[i] = (HANDLE)
            _beginthreadex ( 
                NULL,
                0,				        
                ThreadProc,				  
                NULL,                    
                CREATE_SUSPENDED,		   
                (unsigned *)&dwThreadId[i]   
            );

        if(hThread[i] == NULL)
        {
            _tprintf(_T("Thread creation fault! \n"));
            return -1;
        }
    }

    for(DWORD i=0; i<NUM_OF_GATE; i++)
    {
        ResumeThread(hThread[i]);
    }


    WaitForMultipleObjects(NUM_OF_GATE, hThread, TRUE, INFINITE);

    _tprintf(_T("total count: %d \n"), gTotalCount);

    for(DWORD i=0; i<NUM_OF_GATE; i++)
    {
        CloseHandle(hThread[i]);
    }
  
    DeleteCriticalSection(&hCriticalSection);

    return 0;
}

 

13.2-2 인터락 함수 기반의 동기화

  Note) 원자적 접근이란, 한 순간에 하나의 접근만 허용하겠단 의미

    즉, 한 순간에 하나의 쓰레드만 호출이 완료되도록 허용한단 것.

    특히 InterlockedIncrement() 함수는 한 순간에 둘 이상의 쓰레드가 접근해서

    값이 증가되지 않게끔 보장해 준다는 의미. 좀 더 깔끔하고 편안하게 구현 가능.

    임계 영역이 단순하다면 인터락 함수 기반 동기화 추천.

<hide/>

void IncreaseCount(void)
{
    InterlockedIncrement(&gTotalCount); // ~= ++gTotalCount. 원자적 접근(Atomic Access)을 보장.
}

 

  Note) 과연 어떻게 둘 이상의 쓰레드가 같은 메모리에 접근하는 것을 막을 수 있을까?

    A 쓰레드와 B 쓰레드가 위와 같은 상황이라 해보자. A 쓰레드가 먼저 임계 영역에 진입함.

    가장 단순한 방법은 스케줄러의 동작을 멈춰버리면 됨. 즉 쓰레드 간의 CS가 발생하지 않음.

    그럼 어떻게 멈춰버릴 수 있지? timer Interrupt를 disable 시킴.

    

  Note) Interrupt는 뭐지? HW가 OS에게 "무슨 일이 일어났다!"를 알려주기 위한 개념.

    즉, OS는 시간이 흘러가는 것을 HW로 부터 제공되는 TimerInterrupt를 통해서 알게 됨.

    이때 스케줄러도 시간이 필요함. 근데 TimerInterrupt를 disable해버리면 스케줄러는 CS를 못함.

    그럼 이때 A 쓰레드는 신나서 임계영역을 지나감. 그리고 나가서 TimerInterrupt를 inable 시켜줌.

    

 

 

13.3 커널 모드 동기화

 

Prologue) OS 내용에서 동기화 기법 중, 세마포어가 있고 그 중 바이너리 세마포어를 뮤텍스라 함.

  Windows OS에서도 세마포어와 유사한 기법을 제공하는 것.

  메모리 접근 동기화란, 임계영역에 들어갈 때 열쇠를 획득해서 들어가는 개념이었음.

  그 중 세마포어 기법은, 열쇠가 여러 개 존재하는 기법.

  뮤텍스는 열쇠가 하나의 키만 존재함. 즉, 가장 큰 차이점은 키의 갯수임.

  즉, 세마포어는 임계영역에 들어오는 쓰레드의 갯수를 제한할 수 있음.

 

13.3-1 뮤텍스(mutex)의 생성

  Note)

// If the below function fails, the return value is NULL.
HANDLE CreateMutex(
    LPSECURITY_ATTRIBUTES lpMutexAttributes, // 보안 관리자. 
    // 커널 모드의 동기화 기법이기 때문에, 커널 오브젝트의 생성을 동반하기 때문에 보안관리자 필요.
    // 핸들 테이블의 핸들이 상속 여부를 결정할 때 사용했었음.
    BOOL bInitialOwner,                      // 소유자 지정
    // 열쇠를 만든사람이 초기에 소유하게끔 할거냐 아니면 누구나 소유하게 할거냐 여부.
    LPCTSTR lpName                           // 뮤텍스 이름 지정
    // 이름 관련 개념은 이후에 나옴.
);

 

 

 

13.3-2 뮤텍스(mutex) 기반의 동기화

  Note) 뮤텍스를 커널 오브젝트라고 생각하자.

    WairForSingleObject() 함수에서 빠져나오기 위해서는 뮤텍스가 Signaled 상태여야 함.

    즉, ReleaseMutex() 함수는 뮤텍스를 Signaled 상태로 만드는 함수.

    WaitForSingleObject() 함수는 뮤텍스를 Non-Signaled 상태로 만드는 함수.

    1. 첫 입장 쓰레드는 뮤텍스를 가지고 WaitForSingleObject() 함수를 호출하면

      Signaled 상태이기 때문에 임계 영역에 진입하고, 뮤텍스는 Non-signaled 상태가 됨.

    2. 두 번째 쓰레드도 뮤텍스를 가지고 WaitForSingleObjecT() 함수를 호출하면

      뮤텍스는 아직 Non-signaled 상태이기 때문에 Blocked 상태로 빠짐.

    3. 앞서 진입한 쓰레드가 임계영역에 통과하고 나오면서 ReleaseMutex() 함수를 호출하면

      그제서야 뮤텍스가 Signaled 상태가 되어서 두번째 쓰레드가 임계 영역에 들어가게 되고,

      뮤텍스는 다시 Non-Signaled 상태가 됨.

    즉, 뮤텍스의 Signaled Vs. Non-signaled 상태를 이용해서 임계영역의 동기화를 구현해냄.

출처. 윤성우, "뇌를 자극하는 윈도우즈 시스템 프로그래밍"

 

  Ex)

<hide/>

/*
    CriticalSectionSyncMutex.cpp
    프로그램 설명: 크리티컬 섹션과 뮤텍스 비교. 얼마나 유사한지를 보자.
*/

#include <stdio.h>
#include <windows.h>
#include <process.h>
#include <tchar.h>

#define NUM_OF_GATE		6

LONG gTotalCount = 0;

// CRITICAL_SECTION   gCriticalSection;
HANDLE hMutex;

void IncreaseCount()
{
//	EnterCriticalSection (&gCriticalSection);
    WaitForSingleObject(hMutex, INFINITE);

    gTotalCount++;

//	LeaveCriticalSection (&gCriticalSection);
    ReleaseMutex(hMutex);
}


unsigned int WINAPI ThreadProc( LPVOID lpParam ) 
{ 
    for(DWORD i=0; i<1000; i++)
    {
        IncreaseCount();
    }

    return 0;
} 


int _tmain(int argc, TCHAR* argv[])
{
    DWORD dwThreadIDs[NUM_OF_GATE];
    HANDLE hThreads[NUM_OF_GATE];

//	InitializeCriticalSection(&gCriticalSection);
    hMutex = CreateMutex (
                   NULL,     // 디폴트 보안관리자.
                   FALSE,    // 누구나 소유 할 수 있는 상태로 생성.
                   NULL      // numaned mutex
             );

    if (hMutex == NULL) 
    {
        _tprintf(_T("CreateMutex error: %d\n"), GetLastError());
    }

    for(DWORD i=0; i<NUM_OF_GATE; i++)
    {
        hThreads[i] = (HANDLE)
            _beginthreadex ( 
                NULL,
                0,				        
                ThreadProc,				  
                NULL,                    
                CREATE_SUSPENDED,		   
                (unsigned *)&dwThreadIDs[i]   
            );

        if(hThreads[i] == NULL)
        {
            _tprintf(_T("Thread creation fault! \n"));
            return -1;
        }
    }

    for(DWORD i=0; i<NUM_OF_GATE; i++)
    {
        ResumeThread(hThreads[i]);
    }


    WaitForMultipleObjects(NUM_OF_GATE, hThreads, TRUE, INFINITE);

    _tprintf(_T("total count: %d \n"), gTotalCount);

    for(DWORD i=0; i<NUM_OF_GATE; i++)
    {
        CloseHandle(hThreads[i]);
    }
  
//	DeleteCriticalSection(&gCriticalSection);
    CloseHandle(hMutex);

    return 0;
}

 

 

13.3-3 세마포어(Semaphore)의 생성

  Note) 

// If the below function fails, the return value is NULL.
HANDLE CreateSemaphore(
    LPSECURITY_ATTRIBUTES lpSemaphoreAttribures,
    LONG lInitialCount, // 열쇠의 개수. 보통은 세마포어의 카운트라고 부름.
                        // "세마포어의 값이 3이다" == "열쇠 꾸러미에 열쇠가 3개다."
                        // 즉, 임계영역에 총 3개의 쓰레드가 동시접근 가능하게 하겠단 뜻.
    LONG lMaximumCount, // 세마포어가 가질 수 있는 카운트의 최댓값 지정.
                        // 즉, lInitialCount보다 작은건 말이 안됨. 항상 lInitialCount <= lMaximumCount.
    LPCTSTR lpName      // 잠시 후에 나오는 개념.
);

 

 

 

13.3-4 세마포어(Semaphore) 기반의 동기화

Note) RealeseSemaphore() 함수가 호출 되면, 세마포어 카운트의 값이 하나 증가함.

  WaitForSingleObject() 함수가 호출되면, 세마포어 카운트의 값이 하나 감소함.

  즉, 세마포어는 세마포어 카운트가 0이 되지 않는 이상 Signaled 상태로 빠지지않음.

  다시 말하면 세마포어 카운트의 갯수만큼 WaitForSingleObject() 함수의 호출을 견뎌낼 수 있음.

  이때 세마포어 카운트가 1이면 뮤텍스인거임. 실제 Windows OS에서도 세마포어 카운트의 

  초기값을 1로 둠으로써 뮤텍스와 동일한 기능을 제공함.

  세마포어와 뮤텍스 사이에는 실질적인 차이가 있긴한데 이건 조금 이따가 배움.

출처. 윤성우, "뇌를 자극하는 윈도우즈 시스템 프로그래밍"

 

  Ex)

<hide/>

/*
    MyongDongKyoJa.cpp
    프로그램 설명: 카운트 세마포어에 대한 이해
    시뮬레이션 제한 요소:
        1. 테이블이 총 10개이고, 동시에 총 10분의 손님만 받을 수 있다고 가정한다.
        2. 오늘 점심시간에 식사하러 오실 예상되는 손님의 수는 총 50분이다.
        3. 각 손님들께서 식사 하시는 시간은 대략 10분에서 30분 사이이다.
*/


#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <windows.h>
#include <process.h>
#include <tchar.h>

#define NUM_OF_CUSTOMER 50
#define RANGE_MIN 10
#define RANGE_MAX (30 - RANGE_MIN)
#define TABLE_CNT 10


HANDLE hSemaphore;
DWORD randTimeArr[50];

void TakeMeal(DWORD time)
{
    WaitForSingleObject(hSemaphore, INFINITE);
    _tprintf( _T("Enter Customer %d~ \n"), GetCurrentThreadId());

    _tprintf(_T("Customer %d having launch~ \n"), GetCurrentThreadId());
    Sleep(1000 * time);	// 식사중인 상태를 시뮬레이션 하는 함수.

    ReleaseSemaphore(hSemaphore, 1, NULL);
    _tprintf( _T("Out Customer %d~ \n\n"), GetCurrentThreadId());
}


unsigned int WINAPI ThreadProc( LPVOID lpParam ) 
{ 
    TakeMeal((DWORD)lpParam);
    return 0;
}


int _tmain(int argc, TCHAR* argv[])
{
    DWORD dwThreadIDs[NUM_OF_CUSTOMER];
    HANDLE hThreads[NUM_OF_CUSTOMER];
   
    srand( (unsigned)time( NULL ) );  	// random function seed 설정


    // 쓰레드에게 전달할 random 값 총 50개 생성.
    for(int i=0; i<NUM_OF_CUSTOMER ;i++)
    {
        randTimeArr[i] = (DWORD) (
                ((double)rand() / (double)RAND_MAX) * RANGE_MAX + RANGE_MIN
            );
    }

    // 세마포어 생성.
    hSemaphore = CreateSemaphore (
                   NULL,    // 디폴트 보안관리자.
                   TABLE_CNT,      // 세마포어 초기 값.
                   TABLE_CNT,      // 세마포어 최대 값.
                   NULL     // unnamed 세마포어 구성.
                 );
    if (hSemaphore == NULL) 
    {
        _tprintf(_T("CreateSemaphore error: %d\n"), GetLastError());
    }


    // Customer를 의미하는 쓰레드 생성.
    for(int i=0; i<NUM_OF_CUSTOMER; i++)
    {
        hThreads[i] = (HANDLE)
            _beginthreadex ( 
                NULL,
                0,				        
                ThreadProc,				  
                (void*)randTimeArr[i],                    
                CREATE_SUSPENDED,		   
                (unsigned *)&dwThreadIDs[i]   
            );

        if(hThreads[i] == NULL)
        {
            _tprintf(_T("Thread creation fault! \n"));
            return -1;
        }
    }

    for(int i=0; i<NUM_OF_CUSTOMER; i++)
    {
        ResumeThread(hThreads[i]);
    }

    WaitForMultipleObjects(NUM_OF_CUSTOMER, hThreads, TRUE, INFINITE);

    _tprintf(_T("----END-----------\n"));

    for(int i=0; i<NUM_OF_CUSTOMER; i++)
    {
        CloseHandle(hThreads[i]);
    }
    
    CloseHandle(hSemaphore);

    return 0;
}

 

 

13.3-5 이름 있는 뮤텍스 기반의 프로세스 동기화

  Note) 우리가 배우고 있는 내용은, 둘 이상의 쓰레드 간에 임계영역 동시접근 동기화임.

    그런데, 두 쓰레드가 프로세스 A와 B에 있다면? 가능하냐? -> 가능하다.

    그럼 둘 중 누군가가 뮤텍스, 즉 열쇠를 만들어야 함. 둘 다 만들면 의미 없음.

    그럼, 서로 다른 프로세스 메모리 영역에 있는데도 동기화가 필요한가? -> 나중에 다뤄봄.

    

출처. 윤성우, "뇌를 자극하는 윈도우즈 시스템 프로그래밍"

 

  Note) 일단 프로세스 A의 쓰레드가 뮤텍스를 생성함.

    1. 동기화가 가능하려면, 프로세스 B의 쓰레드도 뮤텍스에 접근이 가능해야 함.

      단순하게 프로세스 A 쓰레드의 핸들테이블에 적힌 뮤텍스의 핸들값을 통신으로 넘겨주면 될까?

      안됨. 아래 예에서는 204라는 숫자인데, 이건 프로세스 A의 핸들테이블에서만 유효한 값임.

      프로세스 B의 핸들테이블에는 의미가 없는 숫자.

    2. 일단 가능한지는 차치하고, 프로세스 A의 핸들테이블에 등록된 뮤텍스의 핸들 정보를

      프로세스 B의 핸들테이블에도 등록이 되어야 함. 같은 프로세스 내에서의 쓰레드 간의 동기화는

      뮤텍스의 핸들을 이용함. 근데 다른 프로세스이기 때문에 뮤텍스의 핸들을 직접 얻을 수 없음.

      즉, 핸들테이블에 뮤텍스에 대한 핸들이 없다는 뜻. 그럼 어떡하냐? 뮤텍스의 이름을 가지고 찾게 됨.

    3. 이전에 CreateMutex()에 이름을 지정할 수 있었음. 이게 바로 서로 다른 프로세스 간의 동기화를 위한 것.

      이 인자가 전달된 뮤텍스를 가르켜, 이름 있는 뮤텍스라 함. 세마포어도 마찬가지로 이름을 줄 수 있음.

    4. 이름을 지정할 수 있는건 이해됨. 그럼 어떻게 이름을 통해 지정 하지?

      함수를 통해서 가능함. OpenMutex() 함수 호출.

출처. 윤성우, "뇌를 자극하는 윈도우즈 시스템 프로그래밍"

 

  Ex) 

<hide/>

/*
    NamedMutex.cpp
    프로그램 설명: named mutex의 역할 이해.
*/

#include <stdio.h>
#include <windows.h>
#include <process.h>
#include <tchar.h>

  
HANDLE hMutex; 
DWORD dwWaitResult;

void ProcessBaseCriticalSection() 
{
    dwWaitResult = WaitForSingleObject(hMutex, INFINITE);

    switch (dwWaitResult) 
    {
        // 쓰레드가 뮤텍스를 소유하였다.
        case WAIT_OBJECT_0:
            _tprintf(_T ("thread got mutex ! \n") );
            break;

        // time-out 발생하였다.
        case WAIT_TIMEOUT: 
            _tprintf(_T ("timer expired ! \n") );
            return; 

        // 뮤텍스 반환이 적절이 이뤄지지 않았다.
        case WAIT_ABANDONED: 
            return; 
    }

    for(DWORD i=0; i<5; i++)
    {
        _tprintf( _T("Thread Running ! \n") );
        Sleep(10000);
    }

    ReleaseMutex(hMutex);
}


int _tmain(int argc, TCHAR* argv[])
{

#if 1  // 1이면 CreateMutex() 호출되고, 0이면 OpenMutex() 호출됨.
    hMutex = CreateMutex( 
                NULL,                       
                FALSE,                      
                _T("NamedMutex")			
            );								
#else

    hMutex = OpenMutex( 
                MUTEX_ALL_ACCESS,      
                FALSE,                 
                _T("NamedMutex")	   
             );  

#endif

    if (hMutex == NULL) 
    {
        _tprintf(_T("CreateMutex error: %d\n"), GetLastError());
        return -1;
    }

    ProcessBaseCriticalSection();

    CloseHandle(hMutex);

    return 0;
}

 

13.3-6 뮤텍스의 소유와 WAIT_ABANDONED

  Note) 소유의 관점에서 세마포어와 뮤텍스의 차이점

    - 이전 NamedMutex.cpp 예제에서, WaitForSingleObject() 함수 호출 후 반환값으로

      WAIT_ABANDONED가 반환될 때가 있음.

    - 뮤텍스의 경우에는, 쓰레드 A가 열쇠를 먼저 얻었다해보자. 

      반환은 임계영역을 통과한 쓰레드 A가 하는 것이 맞음. 당연함.

      근데 임계영역 통과 도중에 쓰레드 A가 어떤 사정으로 반환도 안하고 소멸해버렸다 해보자.

      열쇠를 소유한 쓰레드가 사라지니까, B 입장에선 막연해짐.

    - 이때 WaitForSingleObject(hMutex, INFINITE) 함수를 호출하고 Blocked 상태에 빠져있던

      B 쓰레드에게 반환되는 값이 바로 WAIT_ABANDONED 임. 이는 에러는 아님.

      뮤텍스 반환이 정상적으로 이뤄지지 않았으니까, 그 열쇠를 OS가 쓰레드 B에게 주겠단 의미.

      즉, 임계 영역을 잘 통과하는지 OS가 A 쓰레드를 추적하고 있던 것.

      그러다가 A 쓰레드가 도중에 소멸 되버리면, 쓰레드 B에게 WAIT_ABANDONED가 반환됨.

    - 따라서 WAIT_ABANDONED를 얻었다고 해서 엄청나게 큰 문제가 발생한건 아님. 

      그냥 그에 맞게 대응 코드를 작성하면 될 뿐.

    - 세마포어는 소유의 개념이 없음. 세마포어는 열쇠가 여러 개임.

      열쇠를 하나 증가시킨 쓰레드와 하나 감소시킨 쓰레드가 꼭 같아야 하는건 아님.

      다만 그러면 좋지 않은 구현인건 맞음. 

    - 그리고 위 내용은 Windows OS의 내용임. 전통적인 OS의 철학은 아님.

 

  Ex)

<hide/>

/*
    MUTEX_WAIT_ABANDONED.cpp
    프로그램 설명: 뮤텍스와 관련된 반환값 WAIT_ABANDONED에 대한 설명.
*/

#include <stdio.h>
#include <windows.h>
#include <process.h>
#include <tchar.h>

LONG gTotalCount = 0;
HANDLE hMutex;

unsigned int WINAPI IncreaseCountOne(LPVOID lpParam)
{
    WaitForSingleObject(hMutex, INFINITE);
    gTotalCount++;

    return 0;
}

unsigned int WINAPI IncreaseCountTwo(LPVOID lpParam)
{
    DWORD dwWaitResult = 0;    
    dwWaitResult = WaitForSingleObject(hMutex, INFINITE);
    
    switch (dwWaitResult) 
    {
       case WAIT_OBJECT_0:
           ReleaseMutex(hMutex);
           break; 

       case WAIT_ABANDONED: 
           _tprintf( _T("WAIT_ABANDONED \n") );
           break; 
    }

    gTotalCount++;

    ReleaseMutex(hMutex);
    return 0;
}

int _tmain(int argc, TCHAR** argv)
{
    
    DWORD dwThreadIDOne;
    DWORD dwThreadIDTwo;
    HANDLE hThreadOne;
    HANDLE hThreadTwo;	

    hMutex = CreateMutex (
                   NULL,     // 디폴트 보안관리자.
               FALSE,    // 누구나 소유 할 수 있는 상태로 생성.
               NULL      // numaned mutex
             );

    if (hMutex == NULL) 
    {
        _tprintf(_T("CreateMutex error: %d\n"), GetLastError());
    }


    // 무례한 쓰레드.
    hThreadOne = (HANDLE)_beginthreadex (
        NULL, 0, IncreaseCountOne, NULL, 0, (unsigned *)&dwThreadIDOne 
    );
    
    hThreadTwo = (HANDLE)_beginthreadex (
        NULL, 0, IncreaseCountTwo, NULL, CREATE_SUSPENDED, (unsigned *)&dwThreadIDTwo   
    );

    Sleep(1000);
    ResumeThread(hThreadTwo);

    WaitForSingleObject(hThreadTwo, INFINITE);
    _tprintf(_T("total count: %d \n"), gTotalCount);

    CloseHandle(hThreadOne);  
    CloseHandle(hThreadTwo);  
    CloseHandle(hMutex);

    return 0;
}

 

 

 

댓글