본문 바로가기
운영체제/[ecourse] Windows Programming

6-2. 동기화 개념과 Critical Section

by 헛둘이 2022. 9. 20.
Critical Section
  • n개의 쓰레드가 공통적인 변수(정적 혹은 전역)를 조작할 때 
  • 이 조작하는 공간은 1개의 쓰레드만 지나가야 한다 라고 정해놓은 게 임계영역(Critical Section)이다.
  • 여러 흐름이 일렬로 지나가는 것이므로 직렬화 (Serialize) 된다고도 얘기한다.
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <Windows.h>
#include <process.h>
#include <tchar.h>
using namespace std;

CRITICAL_SECTION cs;

UINT __stdcall foo(void* p)
{
    static int n = 0;

    EnterCriticalSection(&cs);

    //n = 100;
    //임계영역 (Critical Section)

    LeaveCriticalSection(&cs);

    return 0;
}

int main()
{
    InitializeCriticalSection(&cs);

    HANDLE thread1 = (HANDLE)_beginthreadex(0, 0, foo, 0, 0, 0);
    HANDLE thread2 = (HANDLE)_beginthreadex(0, 0, foo, 0, 0, 0);

    HANDLE hArr[2] = { thread1, thread2 };
    
    WaitForMultipleObjects(2, hArr, TRUE, INFINITE);
    DeleteCriticalSection(&cs);

    CloseHandle(thread1);
    CloseHandle(thread2);

    return 0;
}
  1. CRITICAL_SECTION 구조체를 전역에 만듦
  2. 메인 함수에서 InitializeCriticalSection 함수로 초기화를 해줌
  3. 직렬화되어야 할 부분의 시작지점에 EnterCriticalSection 함수를 사용
  4. 끝나고 LeaveCriticalSection을 사용해서 직렬화 영역 해제
  5. 끝나고 DeleteCriticalSection으로 메모리 해제
  6. 위 함수들은 모두 매개변수로 CRITICAL_SECTION의 주소를 받는다.

 

 

Spin Count
  • 쓰레드 2개가 임계 영역을 지날 때에는 1개가 들어가면 다른 1개는 대기 상태에 놓임
  • 운영 체제는 active thread list와 blocked thread list의 링크드리스트를 따로 관리하는데
  • 이 상황에서는 active thread list에 있는 쓰레드를 blocked thread list로 옮김
CPU가 1개일 때는 쓰레드 2개가 동시에 일을 할 때 퀀텀 타임으로 시간을 나눠서 일을 함
그래서 위에서 설명한 active thread list에서 blocked thread list로 쓰레드를 옮길 때 일이 끝날 가능성이 없음
(왜냐하면, 동시에 실행되지 않으므로)
  • 그러나 CPU가 2개 이상일 때는 OS가 쓰레드를 옮길 때 작업이 끝날 수 있으므로,
  • EnterCriticalSection을 한 번 시도하는 것이 아닌 1000~2000번 시도하는 것이 성능 향상에 도움이 된다.

 

  • 멀티코어의 경우 초기화할 때 InitializeCriticalSectionAndSpinCount 함수를 적고, 두 번째 인자로 횟수를 넘겨주면 된다.
  • 그러면 멀티코어에서는 확실히 성능 향상을 볼 수 있음
  • 싱글코어에서는 두 번째 인자는 무시됨

 

 

 

 

 

 

 


Mutex
  • 공유 자원을 하나의 스레드가 독점적으로 사용할 수 있게 해주는 동기화 객체
뮤택스의 실생활 예시)

화장실 1칸이 있는데 당연하게도 한 명씩밖에 사용할 수 없음
열쇠를 가지고 화장실에 들어가서 볼일을 본 후 나와서 다시 걸어두면
다음 사람이 열쇠를 가지고 들어감

여기서 열쇠는 자원을 독점할 수 있는 열쇠

  • Mutex 커널 오브젝트를 만든다.
  • 이 함수의 반환값으로 핸들을 반환하며 KMUTANT라는 이름으로 운영체제가 관리
  • 뮤택스의 특징으로는 소유자와 소유 횟수라는 값을 가진다.
  • lpMutexAttributes - 보안 속성, 0
  • lpName - 정할 이름
  • dwFlags - 이 프로세스가 소유할건지, 안할건지
  • dwDesiredAccess - 권한, MUTEX_ALL_ACCESS

 

  • 소유자가 있으면 논 시그널, 소유자가 없으면 시그널이 된다.
  • WaitForSingleObject로 Mutex를 통과 시 쓰레드가 Mutex를 소유하게 된다.
  • 그러면 시그널 필드가 논 시그널이 되고, 소유자는 내가 되며, 소유 횟수가 1 증가한다.
  • 프로그램을 한 번 더 실행하면 동일한 이름으로 또 뮤택스를 만들텐데
  • 윈도우에서는 동일한 이름의 커널 오브젝트를 만들 수 없다.
  • 그래서 그런 경우 기존 뮤택스를 오픈해주고 참조계수가 1이 증가한다. (MKO)
  • 소유권을 포기하려면 ReleaseMutex를 해주어야 한다. 

 

여러 쓰레드가 대기중인 경우 순서대로 소유권을 가질까?

먼저 내기했던 게 먼저 소유권을 잡는다는 보장이 없다.
대기중인 것 중 하나가 깨어난다 라고 봐야 함
  • 뮤택스를 소유하고 있는 쓰레드가 WaitForSingleObject를 또 하면 그냥 통과한다.
  • 대신 Release도 2번 해줘야 함
  • 뮤택스를 가지고 있는 쓰레드가 소유권을 반환하지 않고 종료된 경우,
  • ABANONED(버려진, 포기된) 뮤택스가 된다.
  • 이 땐 뮤택스의 소유권이 자동으로 포기되는데, 공유 자원에 문제가 있을 수 있음

 

 

 

 

 


Semaphore
  • 위의 뮤택스와 비슷한데 화장실이 n개 있는 형태
  • 자원의 개수를 관리하는 동기화 객체
  • 자원을 한정적인 수량만큼 공유함
  • CreateSemaphoreEx 함수로 만든다.

 

  • 반환값은 커널 오브젝트 KSEMAPHORE의 핸들을 반환한다.
  • 세마포어만의 특징은 현재 카운트와 최대 카운트가 있다.
  • lpSemaphoreAttributes - 보안 속성, 0
  • lInitialCount - 현재 카운트값
  • lMaximumCount - 최대 카운트값
  • lpName - 만들 이름
  • dwFlags - 사용되지 않음, 0
  • dwDesireAccess - 권한, SEMAPHORE_ALL_ACCESS

 

  • WaitForSingleObject를 통과하면 소유권을 갖는다.
  • 그리고 세마포어 객체의 카운트가 -1이 되고, 그 값이 0이면 논 시그널 상태가 된다.
  • ReleaseSemaphore 함수를 통해 소유권을 포기하고, 빈 자리가 생기면 다른 쓰레드가 들어온다.

 

 

 

 

 

 

 


Event
  • 상태를 이용해서 n개의 쓰레드가 서로간 통신할 수 있게 해주는 동기화 객체
  • 두 개의 쓰레드가 있는데,
  • 하나는 그림을 다운로드하는 쓰레드고,
  • 나머지 하나는 그 그림을 출력하는 쓰레드라고 한다면
  • 그림을 출력하는 쓰레드는 그림이 다운로드되었는지 알아야만 한다.
  • 이걸 신호를 기준으로 통신할 수 있는데 이 방식을 이벤트라고 한다.
  • 하나의 쓰레드가 작업이 완료되었음을 다른 쓰레드에게 알리기 위해 사용한다.

 

  • lpEventAttrivutes - 보안 속성, 0
  • lpName - 정할 이름
  • dwFlag - 초기 시그널 상태와 reset의 종류 (0이면 논 시그널 상태에 AUTO RESET을 사용한다는 의미)
  • dwDesiredAccess - 권한, EVENT_ALL_ACCESS
  • 반환값으로 이벤트 커널 오브젝트가 반환됨
  • 이벤트의 고유한 특징으로는 3번째 인자 Reset의 종류가 있다.

 

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <Windows.h>
#include <process.h>
#include <tchar.h>
using namespace std;

HANDLE hEvent = 0;

UINT __stdcall foo(void* p)
{
    WaitForSingleObject(hEvent, INFINITE); // 이벤트가 시그널될 때까지 대기
                                           //일을 하려면 Event객체가 signal 되야 함
                                           // 외부에서 signal상태로 바꿔줘야 함
    printf("foo start work\n");
    return 0;
}

int main()
{
    hEvent = CreateEventEx(0, _T("MyEvent"), 0, EVENT_ALL_ACCESS);

    HANDLE hThread = (HANDLE)_beginthreadex(
        0, 0, foo, 0, 0, 0);

    getchar();
    SetEvent(hEvent); // 이벤트를 signal 상태로 만들기
    getchar();

    CloseHandle(hEvent);
 
    return 0;
}
  • AUTO RESET은 WaitForSingleObject 통과 시 자동으로 시그널 필드가 리셋된다.
  • MANUAL RESET의 경우 WaitForSingleObject 통과해도 시그널 필드 변화 없음
  • 이 때는 ResetEvent 함수를 호출해야 논 시그널 상태로 만들 수 있다.

'운영체제 > [ecourse] Windows Programming' 카테고리의 다른 글

6-2-2. Event 실습  (0) 2022.09.20
6-2-1. Semaphore 실습  (0) 2022.09.20
6-1. Thread Basic  (0) 2022.09.20
5-4. Stack Memory  (0) 2022.09.19
5-3. Heap Memory  (0) 2022.09.18

댓글