728x90

이름있는 뮤텍스(Named Mutex)기반의 프로세스 동기화

뮤텍스나 세마포어를 생성할 때 오브젝트에 이름을 붙여줄 수 있다. 뮤텍스에 이름을 붙여 생성할 경우 "이름있는 뮤텍스(Named Mutex)"라 하고, 세마포어에 이름을 붙여 생성할 경우 "이름있는 세마포어(Named Semaphore)"라고 한다.

 

이름있는 뮤텍스 기반의 동기화는 서로 다른 프로세스 영역에 존재하는 쓰레드를 동기화 시키는데 사용된다.

 

뮤텍스는 커널오브젝트 이므로 모든 프로세스들이 접근할 수 있다. 그러나 뮤텍스의 핸들값은 뮤텍스를 생성한 프로세스만이 소유하고 있고 핸들값을 다른 프로세스에 넘겨줘도 핸들테이블에 등록되어있지 않아 의미가 없다. 핸들값에 대한 정보 즉 실제 커널오브젝트를 가리키는 핸들값은 핸들 테이블에 등록되어 있는데, 이 핸들 테이블은 프로세스 별로 독립적이다.

 

A프로세스에서 이름있는 뮤텍스를 생성 하고 실행중이다. 이후 B프로세스가 뮤텍스의 이름을 설정하고 OpenMutex함수를 호출하여 실행하면 A프로세스가 소유하고 있는 뮤텍스의 핸들을 얻을 수 있다.

B프로세스가 OpenMutex함수를 호출하여 뮤텍스를 소유할려고 하지만 A프로세스가 이미 소유하고 있기 때문에 A프로세스가 뮤텍스를 반환할 때까지 B프로세스는 실행을 멈춘다. A프로세스가 뮤텍스를 반환하면, B프로세스는 뮤텍스를 얻어서 실행하게 된다.

 

위와같은 원리로 이름있는 뮤텍스를 사용하면 서로 다른 프로세스에 존재하는 쓰레드 간에도 동기화가 가능하다.

 

OpenMutex 함수 선언

HANDLE OpenMutex(

    DWORD dwDesiredAccess,

    BOOL bInheritHandle,

    LPCTSTR lpName

);

1.dwDesiredAccess : 이름 있는 뮤텍스로의 접근 권한을 지정하는 것이다. 전달인자로 MUTEX_ALL_ACCESS을 전달해서 접근할 수 있는 권한을 요청해야 한다.

2.hInheritHandle : 핸들의 상속 유무를 결정하기 위한 전달인자 이다.

3.lpName : 얻고자 하는 핸들 정보의 커널 오브젝트 이름을 전달한다. 여기로 전달하는 이름과 일치하는 이름을 지니는 뮤텍스가 존재한다면, 이 뮤텍스의 핸들이 반환된다. 핸들 테이블에 이에 대한 정보도 추가된다.

 

 뮤텍스의 소유와 WAIT_ABANDONED

 

WaitForSingleObject 함수의 반환값중 WAIT_ABANDONED라는 것이 있다.

 

뮤텍스의 경우 획득한 쓰레드가 반환해야 하지만 세마포어는 그렇지 않다. 즉 세마포어의 경우 세마포어를 획득하는 쓰레드와 반환하는 쓰레드가 달라도 문제가 되지 않는다.

 

뮤텍스는 획득한 쓰레드가 직접 반환하는 것이 원칙이다. 본인만이 반환할 수 있다. 그러나 세마포어와 그 이외의 동기화 오브젝트는 마치 도서 대여점처럼 대신 다른 누군가가 반환해 줘도 문제가 되지 않는다.

 

쓰레드 A가 오브젝트인 뮤텍스를 소유하고 있다. 쓰레드B는 쓰레드 A가 뮤텍스를 반환하기를 기다린다. 그러나 예상치 못한 문제로 쓰레드 A가 뮤텍스를 반환하지도 않고 사라졌다. 이러한 경우 Windows는 정상적인 방법으로 반환이 불가능한 뮤텍스를 대신 반환해주고, 다음 대기자인 쓰레드 B가 뮤텍스를 소유할 수 있도록 도와준다. 이 때 쓰레드 B는 WAIT_ABANDONED값을 반환받게 된다.

728x90
728x90

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

세마포어와 뮤텍스는 상당히 유사하다고 한다. 둘의 차이는 카운트(Count)기능이다. 세마포어는 카운트 기능이 존재하지만, 뮤텍스에는 존재하지 않는다.

뮤텍스에는 임계영역에 접근 가능한 쓰레드 개수를 조절하는 기능이 없다. 그러나 세마포어는 임계영역에 접근 가능한 쓰레드 개수를 조절하는 기능이 있다.

 

임계영역의 접근 허용 쓰레드 개수를 하나로 제한하기 위해 사용되는 세마포어를 가리켜 바이너리(Binary)세마포어 라고 한다. 바이너리 세마포어는 뮤텍스와 동일한 기능을 제공하게 된다.

 

세마포어 관련 함수

세마포어(세마포어 오브젝트)를 생성하는 함수

HANDLE CreateSemaphore(

     LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,

     LONG lInitialCount,

     LONG lMaximumCount,

     LPCTSTR lpName

);

1.lpSemaphoreAttributes : 보안 속성을 지정하기 위한 매개변수이다.

2.lInitialCount : 임계영역에 접근 가능한 쓰레드의 개수를 제한한다. 세마포어에서 가장 중요한 전달인자이다.

3.lMaximumCount : 세마포어가 지닐수 있는 값의 최대 크기를 지정한다. 이 값이 1이면 뮤텍스와 동일한 기능을 하는 바이너리 세마포어가 구성된다. 기본적으로 lInitialCount 로 전달되는 값보다 커야한다.

4.lpName : 세마포어에 이름을 붙이기 위해 사용한다.

 

세마포어를 생성할 때 lInitialCount의 값을 10으로 설정한다면 WaitForSingleObject함수가 총 열번 호출될 때까지 카운트가 하나씩 감소하며 함수를 반환한다. WaitForSingleObject함수가 열한번쨰 호출될때 세마포어 카운트가 0인 관계로 블로킹 상태에 빠진다. 세마포어 카운터가 0보다 크면 Signaled 상태이고 0이라면 Non-Signaled 상태가 되는것이다.

 

임계영역을 빠져나온 쓰레드는 ReleaseSemaphore 함수를 호출해야 한다. 이 함수는 세마포어 카운트를 증가시키는 역할을 한다.

BOOL ReleaseSemaphore(

   HANDLE hSemaphore,

   LONG lReleaseCount,

   LPLONG lpPreviousCount

);

1.hSemaphore : 반환하고자 하는 세마포어의 핸들을 인자로 전달한다.

2.lReleaseCount : 증가시킬 값의 크기를 결정할 수 있다. 2를 전달할 경우 세마포어 카운트 2가 증가한다. 아주 특별한 경우가 아니라면 1을 전달하는 것으로 충분하다. 세마포어 생성시 결정한 최대 카운트 값(CreateSemaphore 함수 중 lMaximumCount)을 넘겨서 증가 시킬것을 요구하는 경우 카운트는 변경되지 않고 FALSE만 반환한다.

3.lpPreviousCount : 변경되기 전 세마포어 카운트 값(이 함수 호출을 통해서 증가하기 이전의 값)을 저장할 변수를 지정한다. 필요 없다면 NULL을 전달한다.

 

 

뮤텍스

뮤텍스에서는 뮤텍스락을 하나의 쓰레드만 얻을 수 있다. 즉, 하나의 쓰레드 만이 크리티컬 섹션에 접근가능하다.

 

세마포어

세마포어는 한순간에 크리티컬 섹션에 접근할 수 있는 쓰레드 개수를 지정해 줄 수 있다. 예를들어 동시에 3개의 쓰레드 접근하게 설정이 가능하다. 접근하는 쓰레드를 카운트 하는 기능이 세마포어에 있으며 ReleaseSemaphore함수는 이러한 카운트를 반환한다.

 

WaitForMultipleObject 함수가 관찰할 수 있는 최대 커널 오브젝트 수는 MAXIMUM_WAIT_OBJECTS(현재 64개)로 제한된다.

728x90
728x90

커널모드에서 동작하는 동기화 기법을 커널모드 동기화 기법이라고 한다.

커널모드 동기화는 유저모드 동기화에 비하면 느리다. 유저모드에서 커널모드로, 커널모드에서 유저모드로의 전환이 필요하기 때문이다. 그러나 커널모드 동기화 기법에서는 유저모드 동기화가 제공해주지 못하는 기능을 제공받을 수 있다.

 

뮤텍스(Mutex)기반의 동기화

뮤텍스 기반 동기화 기법의 경우 크리티컬 섹션 오브젝트와 같이 뮤텍스 오브젝트가 존재한다. 이는 크리티컬 섹션 오브젝트와 달리 다음 함수를 통해서 만들어 진다.

HANDLE CreateMutex(

    LPSECURITY_ATTRIBUTES lpMutexAttributes,

    BOOL bInitialOwner,

    LPCTSTR  lpName

);

1.lpMutexAttributes : 뮤텍스도 커널오브젝트 이기 때문에 이 값을 통해 보안속성을 지정해 줄 수 있다.

2.bInitialOwner : 뮤텍스는 뮤텍스 오브젝트를 생성하는 쓰레드에게 임계영역에 접근하는 기회를 먼저 줄 수 있다. FALSE를 전달할 경우 크리티컬 섹션처럼 먼저 접근하는 쓰레드가 임자가 되게할 수 있다. TRUE를 전달할 경우 뮤텍스를 생성하는 쓰레드가 먼저 기회를 얻을 수도 있다. 이를 결정하는 전달인자이다.

3.lpName : 뮤텍스에 이름을 붙여주기 위해 사용한다. 이름은 널(NULL)문자로 끝나는 문자열로 저장하면 된다. 이름을 주었을 때 생성되는 뮤텍스를 가리켜 Named Mutex(이름있는 뮤텍스)라 표현한다.

4.함수의 반환타입 HANDLE : 반환 타입이 HANDLE이라는 것은 뮤텍스가 커널 오브젝트임을 말하는 것이다.

 

뮤텍스가 커널 오브젝트인 점만 보더라도, 뮤텍스는 커널레벨 동기화 기법임을 알 수 있다. 뮤텍스는 함수 호출 과정에서 필요한 모든 초기화가 이루어 진다.

 

 "커널 오브젝트는 상태를 지닌다. 하나는 Signaled 상태이고, 다른 하나는 Non-Signaled 상태이다."

보통 커널오브젝트는 Non-Signaled 상태에 놓여있다가, 특정 상황이되면 Signaled 상태가 된다.

 

뮤텍스는 누군가에 의해 획득이 가능할 때 Signaled 상태에 놓이고 누군가에 의해 획득되어져 있는 상태라면 Non-Signaled 상태가 된다.

 

뮤텍스는 획득이 가능할 때 Signaled 상태에 놓인다. 따라서 WaitForSingledObject 함수를 임계영역 진입을 위한 뮤텍스 획득의 용도로 사용가능하다.

 

뮤텍스를 반환할 때 사용하는 함수(뮤텍스를 Signaled 상태로 설정)

BOOL ReleaseMutex(

   HANDLE hMutex

);

1.hMutex : 반환할 뮤텍스 핸들을 인자로 전달한다. Non-Signaled 상태에 있는 뮤텍스 오브젝트는 Signaled 상태가 된다.

 

WaitForSingleObject 함수의 특성 : WaitForSingleObject 함수는 인자로 전달된 핸들의 커널오브젝트가 Signaled상태가 되어서 반환하는 경우, 해당 커널 오브젝트를 Non-Signaled 상태로 변경해 버린다.

 

뮤텍스 동기화는 ReleaseMutex 함수와 WaitForSingleObject 함수로 조절 가능하다.

 

쓰레드는 임계영역에 들어가기에 앞서 뮤텍스를 획득해야한다. 따라서 뮤텍스 핸들을 인자로 전달하면서 WaitForSingleObject함수를 호출한다. 뮤텍스가 획득가능한 상태라면 Signaled 상태일 것이다. 때문에 뮤텍스를 획득하면서 임계영역에 진입하게 된다. WaitForSingleObject함수는 커널오브젝트가 Signaled 상태가 되어 반환할 경우, 해당 커널오브젝트 상태를 Non-Signaled 상태로 변경하므로, 다른 쓰레드들은 임계영역으로의 진입이 제한된다. 임계영역에서의 일을 마친 쓰레드가 임계영역을 빠져 나오면서 ReleaseMutex함수를 호출한다. 이 함수가 호출되면, 뮤텍스는 다른 누군가에게 획득이 가능한 상태, 즉 Signaled 상태가 되어서 다른 쓰레드의 진입을 허용한다.

 

뮤텍스는 커널 오브젝트이다. 해당 리소스를 제거할 때는 CloseHandle 함수를 통해 반환하면 된다. 소멸은 운영체제가 해준다.

728x90
728x90

유저모드 도기화 기법을 적용할 경우, 커널 모드로의 전환이 불필요 하기 때문에 성능상 이점을 얻을 수 있다. 그리고 커널 모드 동기화에 비해 활용하는 방법도 단순하다.

 

크리티컬 섹션(Critical Section) 기반의 동기화

크리티컬 섹션 기반의 동기화를 사용하려면 크리티컬 섹션 오브젝트를 만들고 초기화 해야한다.

크리티컬 섹션 오브젝트는 자료형 CRITICAL_SECTION의 변수이다.

선언 예시)

CRITICAL_SECTIN gCriticalSection;

 

크리티컬 섹션 오브젝트를 선언한 이후에는 다음 함수를 통해 초기화 과정을 거쳐야 한다.

void InitializeCriticalSection(

   LPCRITICAL_SECTION lpCriticalSection

);    // lpCriticalSection : 초기화 하고자 하는 크리티컬 섹션 오브젝트의 주소를 넘긴다.

위 함수로 초기화 과정을 거쳐야만 크리티컬 섹션 오브젝트는 사용가능한 상태가 된다.

 

크리티컬 섹션 오브젝트 초기화 과정

ex)

CRITICAL_SECTION * gCriticalSection;

 

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

{

    ...

    InitializeCriticalSection(gCriticalSection);

    ...

}

 

크리티컬 섹션에 진입하고 빠져나오는데에 특별한 함수가 필요하다. 크리티컬 섹션 접근 동기화를 위한 함수들이다.

 

크리티컬 섹션 접근함수

void EnterCriticalSection(

   LPCRITICAL_SECTION lpCriticalSection

);

lpCriticalSection : 임계 영역에 진입하기 위해 필요한 크리티컬 섹션 오브젝트의 주소값이 인자로 사용된다. 다른 쓰레드에 의해 이 함수가 호출된 상태라면 호출된 함수는 블로킹 상태가 되고 다른 쓰레드가 반환하면 호출된 함수는 블로킹 상태를 빠져나온다. 위 함수를 호출한 쓰레드가 크리티컬 섹션 오브젝트를 획득했다고 표현한다.

 

크리티컬 섹션 빠져나오는 함수

void LeaveCriticalSection(

  LPCRITICAL_SECTION lpCriticalSection

);

lpCriticalSection : 위 함수는 임계영역을 빠져 나와서 호출하는 함수이다. 다른 쓰레드가 EnterCriticalSection 함수를 호출하고 블로킹 상태에 놓여있다면, LeaveCriticalSection 함수 호출을 통해 현재 쓰레드가 임계영역 접근을 반환하고 블로킹 상태에 놓여있던 다른 쓰레드가 블로킹 상태를 빠져나와 임계영역에 접근한다.

LeaveCriticalSection 함수가 호출이 완료되었을 때 이 함수를 호출한 쓰레드가 크리티컬섹션 오브젝트를 반환했다고 표현한다.

 

 위 두 함수 사용 형태

//임계 영역 진입을 위한 크리티컬 섹션 오브젝트 획득

EnterCriticalSection(&CriticalSection);

                ...

            임계영역

                ...

//크리티컬 섹션 오브젝트 반환(임계영역 빠져나옴)

LeaveCriticalSection(&CriticalSection);

 

임계영역이 결정되면 임계영역 진입 이전에 "EnterCriticalSection" 함수를 호출하고, 임계영역을 빠져나간 이후에 "LeaveCriticalSection"함수 호출로 크리티컬 섹션 오브젝트를 반환한다. 이러한 방법을 통해 임계영역에 한순간에 하나의 쓰레드만 실행할 수 있도록 구성하는 것이 크리티컬 섹션 동기화 기법의 핵심이다.

 

크리티컬 섹션 오브젝트 반환 함수

void DeleteCriticalSection(

    LPCRITICAL_SECTION lpCriticalSection

);

lpCriticalSection : 반환하고자 하는 크리티컬 섹션 오브젝트의 주소값을 인자로 전달한다.

위함수를 통해 크리티컬 섹션 오브젝트를 제거한다.

 

인터락 함수(Interlocked Family Of Function)기반의 동기화

인터락 함수는 내부적으로 한순간에 하나의 쓰레드에 의해서만 실행되도록 동기화 되어있다. 인터락 함수는 전역으로 선언된 변수 하나의 접근 방식을 동기화 하는 것에 특화 되어 있다.

 

인터락 함수

LONG InterlockedIncrement(

    LONG volatile* Addend

);

Addend : 값을 하나 증가시킬 32비트 변수의 주소값을 전달한다. 둘 이상의 쓰레드가 공유하는 메모리에 저장된 값을 이 함수를 통해 증가시킬 경우 동기화된 상태에서 접근하는 것과 동일한 안전성을 보장받을 수 있다.

 

LONG InterlockedDecrement(

  LONG volatile * Addend

);

Addend : 값을 하나 감소시킬 32비트 변수의 주속ㅄ을 인자로 전달한다. 둘 이상의 쓰레드가 공유하는 메모리에 저장된 값을 이 함수를 통해서 감소 시킬 경우, 동기화된 상태에서 접근하는 것과 동일한 안전성을 보장받을 수 있다.

 

InterlockedIncrement 함수와 InterlockedDecrement 함수는 원자적 접근(Atomic Access), 한순간에 하나의 쓰레드만 접근하는것을 보장해주는 함수이다. 크리티컬 섹션 동기화 기법도 내부적으로는 인터락 함수를 기반으로 구현되어있다. 위의 인터락 함수들도 유저모드 기반으로 동작하기 때문에 속도가 상당히 빠르다.

위 함수들을 통해 더 간결하게 안전한 쓰렏 형태를 만들 수 있다.

ex)

void IncreaseCount()

{

  //gTotalCount++;

  InterlockedIncrement(&gTotalCount+);

}

 

마이크로 소프트에서는 더 다양한 인터락 함수들을 제공한다.

원하는 수만큼 값을 증가시키거나 감소시키는 함수, 64비트 변수를 대상으로 연산하는 함수 등등

 

volatile 키워드

volatile 키워드의 의미는 크게 두가지가 있다.

1.최적화를 수행하지 마라

2.메모리에 직접 연산하라

 

1.최적화를 수행하지 마라

컴파일러는 코드를 컴파일 하는 과정에서 코드의 최적화를 수행한다.

ex)

int function(void)

{

  int a=10;

  a= 20;

  a= 30;

 cout << a;

}

--->

int Function(void)

{

     int a= 30;

     cout << a;

}

프로그램  실행결과가 동일하다는 관점에서 컴파일 과정에서의 코드 최적화가 이루어 질수도 있다.

이러한 최적화가 문제가되는 상황이 존재한다.

임베디드 시스템을 구성할 때 메모리 맵(Memory Map)디자인 이라고 하는 과정을 거치게 되는데, 이 과정에서 출력을 위해 사용되는 LCD나 소리를 내기 위해 사용되는 오디오 칩과 같은 하드웨어 장치에도 주소를 할당하게 된다. 즉 메모리 주소가 RAM과 같은 저장장치에만 할당되는 것이아니라, 하드웨어 장치에도 할당된다.

 

위와같은 상황에 최적화를 막기 위해 volatile 키워드를 사용한다.

ex)

int function(void)

{

   int volatile  * psound = 0x30000;

   ...

}

 

2."메모리에 직접 연산하라!"

ex)

int function(void)

{

   int * psound = 0x30000;

   SleepUntil(3, 35, 12);

   *psound = 2;

   ...

}

SleepUntil 이라는 함수를 만들어서 사용한 프로그램이다. 위 프로그램을 3시 35분 12초에 "미"음을 내는 프로그램이다.

3시35분 12초에 "* psound = 2"즉 0x30000에 데이터 2가 입력되면서 '미'음이 발생해야 한다. 그러나 성능향상을 위해 캐쉬메니저가 데이터를 캐쉬 메모리에 저장했다면 우리가 원하는 시점에 '미'음을 들을 수 없다.

(언젠가는 캐쉬에 저장된 데이터가 메모리에 저장되므로 소리는 발생할 것이다.)

 

위와같은 문제를 막기위해 volatile키어드를 사용할 수 있다. volatile로 선언되면 해당 데이터는 절대로 캐쉬되지 않는다. 바로 메모리에 직접 연산하게된다.

 

인터락 함수 인자들이 volatile로  선언되어 있다. 이는 함수 내부적으로 최적화를 수행하지 않으며, 해당 포인터가 가리키는 메모리영역을 캐쉬하지 않겠다는 것을 의미한다.

728x90
728x90

쓰레드 동기화는 일치한다는 의미에서의 동기화가 아니라 순서에 있어서 질서가 지켜지고 있음을 의미한다. 즉 쓰레드동기화는 쓰레드의 실행순서를 정의하고, 이 순서에 반드시 따르도록 하는 것이 쓰레드 동기화 이다.

또한 한순간에 하나의 쓰레드만 접근해야 하는 메모리 영역이 존재한다. (데이터 영역, 힙 영역) 메모리 접근에 있어서 동시 접근을 막는것 또한 쓰레드의 동기화에 해당한다.

"실행 순서의 동기화"와 "메모리 접근의 동기화"가 있다.

 

Windows에서의 동기화 기법

Windows에서의 동기화 기법은 제공하는 주체에 따라 두가지로 나뉜다.

1.유저모드 동기화(User Mode Synchronize) 기법

2. 커널 모드 동기화(Kernel Mode Synchronize)기법 이 있다.

 

유저 모드 동기화 : 동기화 과정에서 커널코드가 실행되지 않는 동기화 기법이다. 동기화 과정에서 커널 모드로의 전환이 없어 성능상 좋다.

 

커널 모드 동기화 : 커널에서 제공하는 동기화 기능을 활용하는 방법이다. 동기화 과정에서 커널 함수를 사용하여 커널모드로의 전환이 필요하고, 성능 저하가 발생한다. 그러나 유저 모드 동기화에서 제공 못하는 기능을 제공받을 수 있다.

 

1.유저 모드 동기화

1-1 크리티컬 섹션(Critical Section) 기반의 동기화

   - 메모리 접근 동기화에 사용 예정

1-2 인터락 함수 (Interlocked Family Of Function)기반의 동기화

   - 메모리 접근 동기화에 사용 예정

 

2.커널 모드 동기화

2-1 뮤텍스(Mutex) 기반의 동기화

   - 메모리 접근 동기화 사용할 예정

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

   - 메모리 접근 동기화에 사용할 예정

2-3 이름 있는 뮤텍스(Named Mutex) 기반의 프로세스 동기화

   - 프로세스간 동기화에 사용할 예정

2-4 이벤트(Event) 기반의 동기화

   - 실행 순서 동기화에 사용할 예정

728x90
728x90

쓰레드의 상태는 계속해서 변경된다. 입력 및 출력 연산을 시작하거나 종료하는 경우, 또는 새로운 쓰레드 생성에 의해서도 변경될 수 있다. 상황이나 운영체제 관리방법에 따라 쓰레드의 상태가 변경되므로 프로그래머가 상태를 직접적으로 컨트롤 하는것이 아니다.

 

그러나 경우에 따라서는 쓰레드의 상태를  프로그래머가 임의로 변경시켜야만 하는 경우도 있을 수 있다. 특정 쓰레드의 상태를 Blocked 상태로 만든다거나, Ready상태로 둔다거나 경우에 따라서 상태를 변경시키는것이 필요할 수 있다.

 

쓰레드의 상태변화

Windows에서는 상태가 변화하는 주체가 프로세스가 아니라 쓰레드이다.

1,2

쓰레드는 생성되면 Ready상태에 놓이게 된다. 이후 스케줄러에 의해서 선택될 경우 Running 상태가 되어 실제 실행이 된다. Ready상태에 놓이는 쓰레드는 여러개일 수 있지만, Running 상태 즉 현재 실행중인 쓰레드는 하나밖에 될 수 없다.

 

3.

실행중인 쓰레드에게 할당된 타임 슬라이스(Time Slice)가 모두 소비되어서, 다른 쓰레드에게 실행기회를 넘겨야 할 때, Running 상태에서 Ready상태로의 이동이 이뤄진다. Ready상태로 이동시켜야만 언제든지 다시 실행될 수 있기 때문에 Ready상태로 이동하는 것이 맞다.

 

4,5

Running 상태에 있는 쓰레드가 입,출력 연산을 하거나, Sleep함수가 호출될 경우 Blocked 상태로 이동하고 다른 쓰레드의 실행을 도모하게 된다. Block 상태가 끝나면 다시 Ready 상태로 돌아가서 실행을 기다린다.

 

Suspend & Resume

쓰레드의 상태 컨트롤

DWORD SuspendThread(

     HANDLE hThread

);

Blocked 상태에 두고자 하는 쓰레드의 핸들을 인자로 전달한다.

 

DWORD ResumeThread(

    HANDLE hThread

);

Ready 상태에 두고자 하는 쓰레드의 핸들을 인자로 전달한다.

 

쓰레드의 커널 오브젝트에는 SuspendThread 함수의 호출 빈도수를 기록하기 위한 서스펜드 카운트(Suspend Count)라 불리는 멤버가 존재한다. 현재 실행중인 쓰레드의 서스펜드 카운트는 0이다.

이 쓰레드 핸들을 인자로 SuspendThread 함수가 호출이 되면, 서스펜드 카운트는 1이되고 쓰레드는 Blocked 상태가 된다. 이 상태에서 SuspendThread함수를 한번더 호출하면 서스펜드 카운트는 2가된다. 즉 SuspendThread 함수는 서스펜드 카운트를 1증가시킨다. ResumeThread함수는 반대로 서스펜드 카운트를 하나 감소시키는 역할을 한다. 즉 위 상황에서는 ResumeThread함수를 두번 호출해야 서스펜드 카운트가 0이되고 Ready상태에 놓이게 된다.

 

SuspendThread 함수와 ResumeThread 함수의 반환값은 모두 변경되기 이전의 서스펜드 카운트를 반환한다.

 

CreateThread의 인자로 CREATE_SUSPENDED가 전달되면, 쓰레드는 생성되자 마자 서스펜드 카운트가 1이다. 즉 쓰레드가 생성되자 마자 Blocked상태가 되는 것이다.

 

쓰레드의 우선순위 컨트롤

 프로세스는 쓰레드를 담는 그릇이다. Window에서는 프로세스가 아닌 프로세스 내부에서 동작하는 쓰레드가 우선순위를 갖는다.

 

쓰레드의 상대적 우선순위           Priority                                                       Meaning

THREAD_PRIORITY_LOWEST                                                                        -2

THREAD_PRIORITY_BELOW_NORMAL                                                            -1

THREAD_PRIORITY_NORMAL                                                                       0(Default)

THREAD_PRIORITY_ABOVE_NORMAL                                                             +1

THREAD_PRIORITY_HIGHEST                                                                        +2

 

 쓰레드의 우선순위는 프로세스의 우선순위와 쓰레드의 우선순위 조합으로 결정된다.

예를 들어 우선순위가 NORMAL_PRIORITY_CLASS(9)인 프로세스 안에 두개의 쓰레드가 존재한다. 각각 쓰레드 우선순위가 THREAD_PRIORITY_LOWEST(-2), THREAD_PRIORITY_NORMAL(0)이라면각쓰레드의우선순위는7(9-2),9(9-0)으로결정된다.

즉 프로세스 우선순위에서 쓰레드 우선순위에 해당하는 값을 더하거나 빼면 쓰레드의 실질적인 우선순위가 나온다.

 

프로세스 내에서 생성되는 모든 쓰레드의 우선순위는 THREAD_PRIORITY_NORMAL이다. 즉 프로세스의 기존 우선순위를 그대로 수용하는 것이다. 이를 변경하거나 참조할때 다음 두 함수를 사용한다.

BOOL SetThreadPriority(

     HANDLE hThread,

     int     nPriority

);

 

int GetThreadPriority(

     HANDLE    hThread

);

728x90
728x90

힙, 데이터 영역, 그리고 코드영역의 공유에 대한 검증

쓰레드는 메모리를 공유한다. 특히 전역변수가 할당되는 데이터 영역과, 메모리가 동적으로 할당되는 힙영역을 공유한다.

 

 

메모리 동시접근 문제

전역변수에 둘 이상의 쓰레드가 동시 접근을 할 수 있다.

실제로는 쓰레드는 돌아가면서 실행된다. 전역변수에 접근하여 연산을 하기 위해서는 메모리에 저장된 데이터를 레지스터로 이동시켜야한다. 이후 ALU에 의해 실질적인 덧셈 연산이 진행되고, 그 결과가 다시 메모리에 저장되는 구조로 진행이딘다. 여러 쓰레드에서 하나의 전역변수를 연산에 사용할 때는 실행중인 쓰레드의 변경에 의해서 컨텍스트 스위칭이 빈번하게 발생한다. 때문에 둘 이상의 쓰레드가 같은 메모리 영역을 동시에 참조하는 것은 문제를 일으킬 가능성이 매우 높다.

 

프로세스로 부터의 쓰레드 분리

프로세스는 쓰레드를 담는 상자역할을 한다. 그리고 핸들 테이블 또한 프로세스 소유이다. 즉 하나의 프로세스에 하나의 핸들 테이블이 존재한다. 또한 같은 프로세스 내에서 생성된 모든 쓰레드들은 스택 이외의 모든것을 공유하기 때문에 핸들 테이블도 공유한다.

 

ANSI 표준 C 라이브러리와 쓰레드

멀티 쓰레드 기반 라이브러리를 설정하고 쓰레드를 생성할 때, CreateTread함수가 아니라 _beginthreadex 함수를 사용해야 한다. _beginthreadex 함수도 내부적으로는 쓰레드 생성을 위해서 CreateThread함수를 호출 한다. 다만 쓰레드를 생성하기에 앞서서 쓰레드를 위해, 독립적인 메모리 블록을 할당해준다는 차이점이 있다. 각각의 메모리 블록을 할당 함으로써 멀티쓰레드의 안정성이 확보된다.

 

_beginthreadex 함수

uintptr_t _beginthreadex(

    void * security,

    unsigned stack_size,

    unsigned (*start_address)(void *),

    void * arglist,

    unsigned initflag,

    unsigned *thrdaddr

);

함수의 전달인자의 순서와 의미는 CreateThread함수와 동일하다. 다만 매개변수 자료형과 반환형에 차이가 있어, 약간의 형 변환이 요구된다.

_beginthreadex 함수를 사용하기 위해서는 헤더파일 process.h를 추가로 포함시켜야 한다.

 

_beginthradex 함수를 종료할 때 가장 좋은 방법은 return문을 이용하는 것이다.

ExitThread 함수를 활용해야할 상황이라면 _endthreadex(unsigned retval);를 사용하는 것이 좋다. _endthreadex이 함수는 내부적으로 쓰레드에 할당된 메모리를 해제하고 ExitThread함수를 호출한다. 쓰레드 함수에서 return 문을 이용하더라도 _endthreadex함수가 자동으로 호출된다. 

728x90

'Programming > Windows System Programming' 카테고리의 다른 글

쓰레드 동기화  (0) 2020.07.28
쓰레드 상태 컨트롤  (0) 2020.07.26
Windows에서의 쓰레드 생성과 소멸  (0) 2020.07.23
쓰레드 구현 모델에 따른 구분  (0) 2020.07.22
쓰레드의 이해  (0) 2020.07.20
728x90

쓰레드 생성

windows에서 쓰레드 생성 함수는 CreateThread이다.

함수 선언

HANDLE CreateThread(

    LPSECURITY_ATTRIBUTES lpThreadAttributes,

   SIZE_T dwStackSize,

   LPTHREAD_START_ROUTINE lpStartAddress,

   LPVOID lpParameter,

   DWORD dwCreationFlags,

   LPDWORD lpThreadId

);

1.lpThreadAttributes : 핸들의 상속 여부 결정, NULL이 전달 되면 상속X

2.dwStackSize : 쓰레드의 스택 크기를 지정하기 위한 매개변수다.

                     0이 전달되면 디폴트 크기인 1M바이트가 적용된다.

3.lpStartAddress : 쓰레드의 main역할을 하는 함수를 지정하는 전달인자이다. 인자 타입이 LPTHREAD_START_ROUTINE                          인데 이는 다음과 같이 정의되어 있는 함수 포인터 이다.

typedef DWORD(WINAPI * PTHREAD_START_ROUTINE)

                     (LPVOID lpThreadParameter);

typedef PTHREAD_START_ROUTINE LPTHREAD_START_ROUTINE;

변환 타입이 DWORD이고 매개변수 타입은 LPVOID(void *)인 형태로 정의되어야 한다.

4.lpParameter : 쓰레드 함수에 전달할 인자를 지정하는 용도로 사용한다. main함수의 argv와 유사하다.

5.dwCreationFlags : 쓰레드의 생성 및 실행을 조절하기 위해 사용되는 전달인자다.

                          인자로 CREATE_SUSPENDED가 전달되면, 쓰레드는 생성과 동시에 Blocked상태에 놓이게 된다.                                 ResumeThread 함수가 호출되면 실행을 시작한다.

                           STACK_SIZE_PARAM_IS_A_RESERVATION을 전달할 경우 dwStackSize를 통해 전달되는 값의                                     크기는 reserve메모리 크기를 의미하게 되고, 그렇지 않을 경우 commit 메모리 크기를 의미하게                               된다.

6.lpThreadId : 쓰레드 ID를 전달받기 위한 변수의 주소값을 전달한다.

7.마지막으로 함수호출이 성공하면 생성된 쓰레드의 핸들이 반환된다.

 

생성할 수 있는 쓰레드의 최대 개수는 메모리가 허용하는 만큼이다. 쓰레드가 생성될 때마다 독립된 스택을 할당해 줘야만 한다. 그렇기 때문에 스택을 할당할 수 있을 때까지 쓰레드의 생성을 허용한다.

main 쓰레드의 return문은 프로세스의 종료를 의미하고 쓰레드 함수의 return문은 쓰레드 종료를 의미한다.

 

멀티 쓰레드 기반 프로그래밍에서 쓰레드의 흐름을 예측하는 것은 불가능 하다. 시스템의 당시 상황에 따라 다르게 동작하기 때문에 예측 자체가 의미가 없다.

 

스택은 쓰레드별로 독립적이기 때문에 쓰레드에 할당해주는 스택이 커질수록 쓰레드의 개수는 줄어든다.

 

Sleep함수

VOID Sleep(

          DWORD dwMilliseconds

);

1.dwMilliseconds : 쓰레드의 실행을 멈추기 위한 시간 정보를 Milliseond 단위로 지정한다. 0을 인자로 전달할 경우 자신에게 할당된 타임 슬라이스(Time Slice)를 포기하고 우선순위가 같은 다른 쓰레드에게 실행의 기회를 양보한다.

 

쓰레드 소멸

쓰레드 함수 내에서 return문을 통해 종료 및 소멸시키는 방법이 가장 이상적이다. 일반적으로 쓰레드 종료 방법에 있어서 return 이외의 방법은 거의 생각하지 않는다. 그러나 return외의 종료방법이 적절한 경우도 존재한다.

 

case 1 : 쓰레드 종료시 return 을 이용하면 좋은 경우

일반적인 경우에 return을 종료로 사용하는 것이 좋다. 특정한 연산의 결과값을 반환 할때도 return을 사용하는 것이 좋다. 쓰레드 함수가 반환한 결과값은 프로세스 종료코드 처럼 커널 오브젝트에 결과가 저장된다.

저장된 종료코드는 main쓰레드에서 GetExitCodeThread함수를 통해 가져올 수 있다.

BOOL GetExitCodeThread(

      HANDLE hThread,

      LPDWORD lpExitCode

);

1.hThread : 종료코드를 얻기 위한 쓰레드의 핸들을 인자로 전달한다.

2.lpExitCode : 얻게되는 종료코드를 저장할 메모리의 주소값을 전달한다.

 

case 2 : 쓰레드 종료 시 ExitThread 함수 호출이 유용한 경우(특정 위치에서 쓰레드의 실행을 종료시키고자 하는 경우)

ExitThread 함수가 존재한다. 이 함수는 현재 실행중인 쓰레드를 종료하고자 할 때 호출하는 함수로 return 방식의 쓰레드 종료만큼 선호된다.

VOID ExitThread(

      DWORD dwExitCode

);

dwExitCode : 커널 오브젝트에 등록되는 쓰레드 종료코드(Exit Code)를 지정

                  위 함수의 장점은 언제 어디서나 쓰레드를 종료시킬 수 있다는 점이다. 그렇다면 return 종료와 무엇이                        다른가.

 

예를 들어 C함수가 호출된 상태에서 쓰레드를 종료해야 한다면, ExitThread함수를 호출하는 것이 더 간결하다. return에 의한 종료를 원한다면 threadfunction까지 return을 해야만 종료가 가능해진다.

 

case 3 : 쓰레드 종료 시 TerminateThread 함수 호출이 유용한 경우(외부에서 쓰레드를 종료하고자 하는 경우)

main함수에서 쓰레드를 생성할 경우 해당 쓰레드의 핸들을 얻게된다. 이 핸들을 이용해서 쓰레드를 강제 종료시킬 수 있다. 순전히 외부에 의한 강제 종료이다.

BOOL TerminateThread(

     HANDLE hThread,

     DWORD dwExitCode

);

1.hThread : 강제 종료할 쓰레드 핸들.

2.dwExitCode : 종료할 쓰레드의 종료코드를 인자로 전달한다. 이 종료코드는 해당 쓰레드의 커널 오브젝트에 등록된다.

 

위 함수의 문제점은 강제 종료라는 점이다. 종료의 대상이 되는 쓰레드는 종료가 되는 시점까지도 자신이 종료된다는 것을 인식하지 못한다. 따라서 종료에 필요한 여러가지 일들(메모리 해제, 리소스 해제 등등)을 처리하지 못하고 종료된다.

728x90

'Programming > Windows System Programming' 카테고리의 다른 글

쓰레드 상태 컨트롤  (0) 2020.07.26
쓰레드의 성격과 특성  (0) 2020.07.25
쓰레드 구현 모델에 따른 구분  (0) 2020.07.22
쓰레드의 이해  (0) 2020.07.20
함수 호출 규약  (0) 2020.07.18

+ Recent posts