[OS] 동기화 객체의 종류

GonnabeAlright·2021년 11월 24일
0
post-thumbnail

동기화 객체의 종류

스레드 동기화 방법

  1. 실행 순서의 동기화
  • 스레드의 실행순서를 정의하고, 이 순서에 반드시 따르도록 하는 것
  1. 메모리 접근에 대한 동기화
  • 메모리 접근에 있어서 동시접근을 막는 것
  • 실행의 순서가 중요한 상황이 아니고, 한 순간에 하나의 스레드만 접근하면 되는 상황을 의미

동기화 기법의 종류

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

유저 모드 동기화

  • 동기화가 진행되는 과정에서 커널 코드가 실행되지 않는 동기화 기법
  • 동기화를 위해서 커널 모드로의 전환이 불필요하기 때문에 성능에 이점이 있다.
  • 하지만, 그만큼 기능상의 제한도 존재한다.

커널 모드 동기화

  • 커널에서 제공하는 동기화 기능을 활용하는 방법
  • 동기화에 관련된 함수가 호출될 때마다 커널 모드로의 변경이 필요하고, 이는 성능의 저하로 이어짐.
  • 하지만 그만큼 유저 모드 동기화에서 제공하지 못하는 기능을 제공받을 수 있다.

유저 모드 동기화와 커널 모드 동기화 모두 각각의 장단점이 존재한다.

동기화 기법(유저 모드 동기화)의 종류

  1. 크리티컬 섹션(Critical Section) 기반의 동기화 (메모리 접근 동기화)
  2. 인터락 함수(Interlocked Family Of Function) 기반의 동기화 (메모리 접근 동기화)

동기화 기법(커널 모드 동기화)의 종류

  1. 뮤텍스(Mutex) 기반의 동기화 (메모리 접근 동기화)
  2. 세마포어(Semaphore) 기반의 동기화 (메모리 접근 동기화)
  3. 이름있는 뮤텍스(Named Mutex) 기반의 동기화 (프로세스 간 동기화)
  4. 이벤트(Event) 기반의 동기화 (실행순서 동기화)

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

  • 임계 영역을 화장실에 비유하면, 화장실에 들어가기 위해서는 화장실 앞에 걸려있는 열쇠를 가져가야만 한다. 열쇠가 걸려있다면 이 열쇠로 문을 열고 화장실에 들어가면 된다. 화장실 사용이 끝났다면 다시 열쇠를 화장실 앞에 걸어 놓는다.
  • 핵심은 열쇠를 얻은 사람만이 화장실에 들어갈 수 있다는 것이다.
// 크리티컬 섹션 기반의 동기화를 사용하기 위한 크리티컬 섹션 오브젝트 생성 및 초기화, 화장실 열쇠
CRITICAL_SECTION gCriticalSection;	// critical section object

// 앞서 크리티컬 섹션 오브젝트를 선언한 후에는 반드시 아래 함수를 통해서 초기화 과정을 거쳐야만 
// 크리티컬 섹션 오브젝트가 사용 가능한 상태가 됨.
void InitializeCriticalSection (
	LPCRITICAL_SECTION lpCriticalSection
    // 초기화하고자 하는 크리티컬 섹션 오브젝트의 주소값을 인자로 전달
);

// 앞서 크리티컬 섹션 오브젝트를 선언하고 초기화하여 사용 가능한 상태가 되었으므로, 이는 
// 화장실에 들어가기 위해 열쇠를 화장실 앞에 걸어놓은 상황이다.
// 이제 화장실에 들어가기 위해서는 열쇠를 사용해야 한다.
// 이 때, 우리가 취해야 할 행동은 두 가지이다.
// 첫번째로 화장실에 들어가기 위해 열쇠를 획득하는 행위이다.

void EnterCriticalSection(
	LPCRITICAL_SECTION lpCriticalSection
)

// 만약 다른 쓰레드에 의해서 이미 이 함수가 호출된 상태라면, 호출된 함수는 블로킹 상태가 된다. 
// 따라서, 함수의 호출에 성공하고 임계 영역으로 들어갔을 때 이를 호출한 쓰레드가 크리티컬 섹션 
// 오브젝트를 획득했다고 표현한다.

// 앞서 열쇠를 획득하여 화장실에 들어갔다.
// 이제 우리가 취해야 하는 두번째 행동은 다음 사람이 화장실에 들어가기 위해서 화장실에서 나온 후 
// 열쇠를 제자리에 걸어두는 행위이다.

void LeaveCriticalSection(
	LPCRITICAL_SECTION lpCriticalSection
)    

// 앞서 EnterCriticalSection 함수 호출로 인해 블로킹 상태에 놓인 쓰레드가 있다면, leaveCriticalSection 함수 호출로 인해서 블로킹 상태에서 빠져나와 임계 영역으로 진입하게 된다. 

// 블로킹 상태에서 빠져나왔다는 것은 열쇠를 획득했다는 뜻이고, 이 함수 호출이 완료되면 쓰레드가 크리티컬 
// 섹션 오브젝트를 반환했다고 한다.

// 임계 영역 진입을 위해서 크리티컬 섹션 오브젝트 획득
EnterCriticalSection(&CriticalSection);
	
    		// 임계영역 🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥

// 크리티컬 섹션 오브젝트 반환
LeaveCriticalSection(&CriticalSection);


// 위의 코드 예시처럼, 임계 영역이 결정되면 'EnterCriticalSection 함수와
// LeaveCriticalSection 함수 호출을 통해서 중간의 임계 영역은 한 순간에 하나의 쓰레드만 
// 실행할 수 있도록 구성하는 것이 바로 크리티컬 섹션 동기화 기법의 핵심이다.

// 끝으로 초기화 함수가 호출되는 과정에서 할당된 리소스들을 아래 함수를 통해 반환해야 한다.
void DeleteCriticalSection(
	LPCRITICAL_SECTION lpCriticalSection
)

인터락 함수 기반의 동기화

  • 앞의 예제와 같이 전역으로 선언된 변수 하나의 접근 방식을 동기화하는 것이 목적이라면, 이러한 용도의 특화된 인터락 함수를 사용하는 것도 좋다.
  • 인터락 함수는 함수 내부적으로 한 순간에 하나의 쓰레드에 의해서만 실행되도록 동기화되어 있다.
LONG InterlockedIncrement(
	LONG volatile* Addend
);
// 값을 하나 증가시킬 32비트 변수의 주소값을 전달한다, 둘 이상의 쓰레드가 공유하는 메모리에 저장된 값을
// 이 함수를 통해서 증가시킬 경우, 동기화된 상태에서 접근하는 것과 동일한 안정성을 보장받을 수 있다.

LONG InterlockedDecrement(
	LONG volatile* Addend
);

// 값을 하나 감소시킬 32비트 변수의 주소값을 인자로 전달한다. 둘 이상의 쓰레드가 공유하는 메모리에 
// 저장된 값을 이 함수를 통해서 감소시킬 경우에, 동기화된 상태에서 접근하는 것과 동일한 안정성을 
// 보장받을 수 있다.
  • 위의 두 함수는 원자적 접근(Atomatic Access), 즉 한 순간에 하나의 쓰레드만 접근하는 것을 보장해주는 함수이다. 따라서 모든 쓰레드가 이 함수들을 통해서 값을 하나 증가시키거나 감소시킬 경우, 동시에 둘 이상의 쓰레드 접근에 의한 문제는 결코 발생하지 않는다.

커널 모드 동기화

  • 커널 모드 동기화에는 크게 두가지의 동기화 기법이 있다.
  • 세마포어 & 뮤텍스

    동기화 기법의 이름은 세마포어이고, 이 세마포어 중 바이너리 세마포어를 뮤텍스라고 한다.

  • 위 두 가지 동기화 방법의 차이점은
    • 크리티컬 섹션 기반 동기화에서 임계영역에 진입하기 위한 key를 얻어야 한다고 했다.
    • 뮤텍스 동기화는 key가 1개이다.
    • 세마포어 동기화는 key가 여러 개 존재한다.
    • 세마포어는 임계영역에 들어오는 쓰레드의 개수를 제한할 수 있다.

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

  • 앞서 유저 모드 동기화에서 '크리티컬 섹션'의 열쇠에 비유된 것이 크리티컬 섹션 오브젝트였다.
  • 뮤텍스 기반 동기화 기법의 경우에 열쇠에 비유할 수 있는 것은 뮤텍스 오브젝트이고, 이는 크리티컬 섹션 오브젝트와 다르게 함수를 통해서 만들어진다.
HANDLE CreateMutex (
	LPSECURITY_ATTRIBUTES lpMutexAttributes,	// 보안 설정
    BOOL bInitialOwner,							// 소유자 지정
    LPCTSTR lpName								// 뮤텍스 이름 지정
);

// 보안설정: 프로세스도 커널 오브젝트이고, 뮤텍스도 커널 오브젝트이므로 마찬가지로 보안 속성을 지정해 줄 수 있다. 
// 이 때, 새로운 프로세스 생성 시 핸들을 상속해 줄 것이냐, 말 것이냐를 결정하는데 있어서 위 전달인자를 활용한다.

// 소유자 지정: 앞서 크리티컬 섹션 기반의 동기화에서는 열쇠를 먼저 소유하는 쓰레드가 임계 영역에 접근할 수 있는 권한을 얻었다. 
// 하지만 뮤텍스는 뮤텍스 오브젝트를 생성하는 쓰레드에게 기회를 먼저 줄 수도 있고, 누구나 소유할 수 있도록 설정할 수 있다.

커널 오브젝트의 상태

  • 커널 오브젝트는 상태를 지니는데, 그 하나는 Signaled 상태이고, 다른 하나는 Non-Signaled 상태이다.
  • 보통 커널 오브젝트는 Non-Signaled 상태에 놓여있다가, 특정 상황이 되면 Signaled 상태가 된다.
  • 이러한 특정 상황이라는 것은 커널 오브젝트에 의존적이다.
  • 뮤텍스는 열쇠에 비유된다.
  • 따라서, 누군가가 열쇠를 취득했을 때, Non-Signaled 상태가 되고
  • 취득한 열쇠를 반환했을 때, Signaled 상태가 된다.
  • 뮤텍스는 누군가에 의해 획득이 가능할 때 Signaled 상태에 놓인다.
  • 크리티컬 섹션 오브젝트를 획득하거나 반환할 때, 사용하는 EnterCriticalSection 함수와 LeaveCriticalSection 함수의 역할을 하는 뮤텍스 관련 함수들이 존재한다.
  • WaitForSingleObject: 키(뮤텍스)를 획득
  • ReleaseMutex: 키(뮤텍스)를 반납
  • 키(뮤텍스)를 커널 오브젝트라고 생각해보면
  • WaitForSingleObject 함수는 Signaled 상태의 뮤텍스 ➡️ Non-Signaled 상태의 뮤텍스
  • ReleaseMutex 함수는 Non-Signaled 상태의 뮤텍스 ➡️ Signaled 상태의 뮤텍스

실행 순서

1. 쓰레드가 Signaled 상태에서 키(뮤텍스)를 가지고 WaitForSingleObject 함수를 호출

  • WaitForSingleObject는 해당 커널 오브젝트가 Signaled 상태가 되는 것을 기다리는 함수이므로
  • Signaled 상태가 되면 WaitForSingleObject 함수를 빠져나온다.
  • WaitForSingleObject 함수를 빠져나왔다면 임계 영역으로 진입하고, 뮤텍스를 NonSignaled 상태로 변경

2. 뮤텍스가 NonSignaled 상태

  • 다음에 실행될 쓰레드는 WaitForSingledObject를 호출하고 대기

3. 앞서 진입한 쓰레드가 임계영역을 빠져 나올 때, ReleaseMutex 함수를 호출

  • ReleaseMutex는 뮤텍스를 Signaled 상태로 만드는 함수
  • WaitForSingleObject 함수를 호출하고 대기중이던 쓰레드는 WaitForSingleObject 함수를 반환하면서, 임계 영역에 진입
  • 뮤텍스는 다시 NonSignaled 상태가 된다.

✅ 즉, 뮤텍스(커널 오브젝트)의 Signaled, Non-Signaled 상태를 이용해서 임계영역의 동기화를 처리한다.

세마포어 기반의 동기화

  • 뮤텍스에는 임계 영역에 접근 가능한 쓰레드 개수를 조절하는 기능이 없다.
  • 그러나 세마포어는 가지고 있다.
HANDLE CreateMutex(
	LPSECURITY_ATTRIBUTES lpMutexAttributes,	// 보안 설정
    LONG lIntialCount,							// 세마포어 초기값(키 개수)
    LONG LMaximumCount,							// 최대 키 개수
    LPCTSTR lpName								// 세마포어 이름 지정     
)

// 세마포어는 카운트를 지니는데, 세마포어가 생성될 때 lInitialCount에 의해서 초기 카운트가 결정됨.
// 초기 카운트 0: Non0Signaled 상태
// 초키 카운트 1: Signaled 상태
// 초기 카운트 10이면, WaitForSingleObject 함수를 호출하여 세마포어 핸들을 인자로 전달하면서 
// 그 값이 하나씩 감소하면서 함수를 반환하고 11번 째 호출 시 세마포어 카운트가 0인 관계로 블로킹 
// 상태에 들어감. 따라서, 총 열개의 쓰레드가 임계 영역에 동시에 들어갈 수 있음.

실행순서

  • ReleaseSemaphore: 세마포어 카운트 + 1
  • WaitForSingleObject: 세마포어 카운트 - 1

1. 세마포어 카운트 10 = 키(뮤텍스)가 10개 = initialCount가 10

  • WaitForSingleObject 함수가 호출되고, 세마포어 카운트가 0이 아니면 WaitForSingleObject가 여러 번 호출되어도 블로킹 상태로 빠지지 않음.
  • Non-Singled 상태가 되지 않는다.
  • 세마포어 카운트가 0이 아닌 값을 가지고 있는 동안에는 Signaled 상태

2. 세마포어가 Signaled 상태

  • WaitForSingleObject 함수가 호출되어도 블로킹 상태로 빠지지 않음.
  • 계속 쓰레드가 동시에 임계 영역으로 진입

3. 세마포어 카운트가 10

  • 10번이 지나면 세마포어 카운트는 NonSignaled 상태가 됨.
  • 그 다음 WaitForSingleObject를 호출하는 쓰레드는 블로킹 상태가 됨.

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

  • 뮤텍스 오브젝트, 세마포어 오브젝트의 생성함수를 자세히 보면, 이름을 붙여줄 수 있도록 디자인 되어있다.

    • 뮤텍스에 이름을 붙여 생성할 경우 "이름있는 뮤텍스"라 한다.
    • 세마포어에 이름을 붙여 생성할 경우 "이름있는 세마포어"라 한다.
  • 그렇다면, 이렇게 이름이 붙은 동기화 오브젝트는 어떠한 용도로 사용될까?

    • 이름있는 뮤텍스만 알고 넘어가자.
    • 뮤텍스는 프로세스가 아니라 커널, 즉 운영체제의 소유이다.
  • 따라서 서로 다른 프로세스 영역에 존재하는 쓰레드가 뮤텍스를 이용해서 동기화 하는 상황이 가능할까 ?

    • 뮤텍스는 커널 오브젝트이므로, 어떤 프로세스의 요청에 의해 만들어졌다 하더라도 그 프로세스의 영역에 존재하는 것이 아니다.
    • 즉, 커널이 관리(생성 및 소멸의 시기를 결정)하는 오브젝트이므로 다른 프로세스에서도 접근 가능하다.
  • 그러나 핸들의 유효성이라는 문제가 있다.

    • 핸들 테이블은 커널 오브젝트와 이를 지칭하는 핸들 값에 대한 정보를 담고 있는 테이블이다.
    • 이는 각각의 프로세스별로 독립적이다.

  • 위 사진과 같은 상황을 생각해보자.
    • 프로세스 A가 뮤텍스를 생성했다면 ?
    • 프로세스 B는 프로세스 A가 생성한 뮤텍스에 접근이 불가능하다.
    • 왜냐하면, 프로세스 A를 통해서 만들어진 커널 오브젝트인 관계로, 프로세스 B의 핸들테이블에는 이에 대한 정보가 없기 때문이다.
  • 그렇다면, 프로세스 A가 핸들값인 204(뮤텍스의 핸들값)을 프로세스 B에게 전달해주면 될까 ??
    • 아니다.
    • 왜냐하면, 상속관계가 없는 서로 다른 프로세스끼리의 핸들 테이블은 독립적이다.
    • 따라서 핸들 테이블에 있는 핸들 값이 같은 값이 나와도 우연의 일치라고 봐야한다.
  • 위의 문제를 해결하기 위해 이름있는 뮤텍스가 나왔다.
    • 이 이름은 Windows라는 운영체제 내에서 유일한 이름이다.
    • 따라서 이 이름을 통해 Windows가 관리하고 있는 커널 오브젝트에 접근 가능한 핸들 정보를 얻을 수 있다.

실행순서

1. 프로세스 A가 뮤텍스를 만들었다.

  • 뮤텍스가 프로세스 A에서 생성되었으므로, 프로세스 A 내부에 있는 쓰레드 A는 이 뮤텍스에 접근이 가능하다.
  • 이 때, 동기화가 가능해지려면 프로세서 B 내부에 있는 쓰레드 B도 이 뮤텍스에 접근이 가능해야 한다.
  • 하지만 접근이 불가능하다.

2. 프로세스 A의 핸들 테이블의 핸들 인자는 프로세스 A 내에서 의미가 있다.

  • 프로세스 B의 핸들 테이블에는 이 뮤텍스에 대한 정보가 없기 때문에, 접근이 불가능하다.

3. 프로세스 B는 뮤텍스 이름으로 뮤텍스를 찾아서 프로세스 B의 핸들 테이블에 이 뮤텍스 정보를 등록한다.

  • 뮤텍스를 생성하는 함수를 보면 뮤텍스 이름을 지정할 수 있다.
  • 뮤텍스 이름을 지정했다는 것은 "내가 현재 속해있는 프로세스가 아닌 다른 프로세스에 존재하는 쓰레드와도 동기화 하겠다"는 의미이다.

위의 예시처럼 세마포어도 마찬가지이다. 세마포어도 생성할 때, 이름을 지정해서 자신이 속한 프로세스와 다른 프로세스에 존재하는 쓰레드 간에도 동기화가 가능하게 할 수 있음

뮤텍스 소유와 WAIT_ABANDONED

  • 세마포어와 뮤텍스 간의 차이점

    1. 키의 개수
    2. 소유 개념

쓰레드 A쓰레드 B가 있을 때, 쓰레드 A가 열쇠를 얻었다면 쓰레드 A가 반환하는게 당연하다.
하지만, 쓰레드 A가 열쇠를 얻어 임계 영역안에 있고 쓰레드 B는 열쇠를 얻지 못해서 WaifForSingleObject 함수를 호출하고 임계 영영 시작 전 지점에서 대기하고 있다.
이 상태에서 쓰레드 A에 문제가 있어서 열쇠를 반환하지 않고 종료된다면 대기중인 쓰레드 BWaitForSingleObject 함수의 반환 값으로 WAIT_ABANDONED을 얻는다.

이는 OS가 쓰레드 A에게 문제가 생겼음을 인지하고, 쓰레드 B에게 열쇠를 전달한다.
비정상 종료에 의해서 뮤텍스(키)의 반환이 제대로 이루어지지 않았으므로, 그 키를 OS가 다시 가져다가 쓰레드 B에게 가져다 주는 형식이다.

✅ 뮤텍스에는 이런 소유 관점이 있지만 세마포어에는 없다.

  • 세마포어는 열쇠를 여러 개 가질 수 있다.
  • 세마포어는 뮤텍스의 카운트 값을 기준으로 공유한다.
  • 따라서, 세마포어 카운트 값을 1 감소시키는 대상과 세마포어 카운트 값을 1 증가시키는 대상이 일치하지 않아도 문제가 발생하지 않는다.

0개의 댓글