동기화

2015. 7. 23. 02:55프로그래밍/서버

728x90
728x90

동기화

스레드나 프로세스간에 공유자원의 접근을 제어하는 방법.


커널 객체

커널에서 관리되는 객체를 의미한다.

종류로는 파일객체, 이벤트 객체, 뮤텍스 객체, 스레드 객체, 세마포어 객체 등이 있다.


커널 객체의 특징

- 다른 객체와 접근하는 방법이 다르다. 실제로 생성하는 것은 커널이며 프로그래머는 요청을 하는 것이다.

메모리 공간에 객체를 할당하고 이를 식별하기 위한 '핸들'을 프로그래머에게 반환해준다.

- 소멸 시점이 다르다. 일반 객체는 프로세스가 소멸시 종료하지만

커널 객체는 공유하고 있는 프로세스가 있다면 (usage count가 0이 아님) 소멸하지 않는다.

- 보안 기술자(security descriptor)를 이용하여 보호받을 수 있다.


커널모드와 유저모드

윈도우즈가 프로세스를 실행하는데 두 가지 모드로 나뉜다.

유저모드는 일반적으로 프로그래머가 메모리를 할당/해제, 함수 실행이 일어나는 모드. 즉, 작성한 코드들은 유저 모드에서 실행.

커널 모드는 프로그래머가 느끼지 못하는 사이에 전환되어 유저 모드에서 할 수 없는 일을 해준다.(커널 객체)


- 커널에 접근할 수 있는 특정한 함수를 제공한다.

- 이 함수들은 유저모드에서 사용되는 것 처럼 보이지만 커널이 이 함수의 요청을 받고 자동으로 커널 모드로 전환해주게 된다.

- 작업이 끝나면 다시 유저모드로 전환.

- 이러한 전환은 속도 저하가 있지만 시스템 코드를 잘못 코딩하는 프로그래머의 잘못된 코드로 인해 

발생하는 문제(시스템 다운, 프로그램 다운)를 막을 수 있다. 분리의 이점은 보안상으로도 중요한 의미를 가진다.

- 불필요하다면 사용을 피하는 것이 좋다.


유저모드 동기화

커널의 도움 없이 동기화를 한다는 의미. 유저모드-커널모드 전환이 없으므로 성능이 좋다.

하지만 한 프로세스내에 있는 스레드간의 동기화만 가능하다.


Interlocked 계열 함수

공유 변수의 원자성을 유지시켜준다.

- 코딩 상으론 cnt++와 같이 하나의 명령이지만 컴퓨터 내부적으론 3개의 기계어 명령어가 된다.

실행되는 중간에 타임 슬라이스가 다 되면 바로 컨텍스트 스위칭이 일어나고, 원하는 결과가 나오지 않을 수 있다.

- cnt++의 원자성을 유지시키면 된다.


CRITICAL_SECTION

인터락과 비슷하지만 여러 줄을 동기화 시켜줄 수 있다.

이런 다른 스레드의 영향을 받지 안흔ㄴ 것을 임계영역이라고 부른다.


- InitializeCriticalSectino() : 크리티컬 섹션 객체를 초기화 시켜준다.

- DeleteCriticalSection() : 크리티컬 섹션 객체를 해제해준다.

- EnterCriticalSection() : 지정된 크리티컬 섹션 객체의 소유권을 가진 스레드에게 할당한다.

- LeaveCriticalSection() : 소유권을 해제한다.


기아상태(starvation)

: 공유 자원을 계속해서 얻지 못하는 상태.


교착상태(Deadlock)

: 두 스레드가 서로 자원을 얻기 위해 무한대기 하는 상태.

ex ) A,B 스레드가  Q,W라는 크리티컬 섹션 객체를 가지고 있을 때,

A는 Q를 소유하고 있고 B는 W를 소유하고 있는데,

A는 W를 원하고 B는 Q를 원할 때, 교착상태라고 한다.


교착상태에 빠지는 이유

- Enter, Leave 짝을 맞추지 못해 기아상태나 교착상태에 들어간다.(프로그래머의 실수)

- 크리티컬 섹션 객체의 짝과 순서를 맞추었다고 하여도 컨텍스트 스위칭으로 인해 교착상태에 빠질 수 있다.(찾기 어려운 버그)


SpinCount

크리티컬 섹션 객체의 SpinCount를 이용하는 방법을 MS는 권장하고 있다.(코어가 2개 이상인 컴퓨터일때 성능이 좋다.)

교착 상태가 일어나려고 할 때, 스레드는 바로 대기 상태에 들어가는 것이 아니라 SpinCount 만큼 시도를 했다가

만약 소유권이 획득되면 진행하고 획득을 실패하면 대기 상태로 들어간다.


예제(스레드 3개로 1~100까지 출력)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#include <windows.h>
#include <process.h>
#include <iostream>
int g_int = 0;
CRITICAL_SECTION cri;
//InitializeCriticalSection(&cri);
unsigned int __stdcall threadFunc(void* lpVoid)
{
    while(true)
    {
        // 임계영역 시작
        EnterCriticalSection(&cri);
        g_int++;
        std::cout<<g_int<<std::endl;
        // 종료 체크
        if (g_int == 98 ||g_int == 99 || g_int == 100)
        {
            break;
        }
        // 임계 영역 끝
        LeaveCriticalSection(&cri);
    }
    // 임계 영역 끝
    LeaveCriticalSection(&cri);
    std::cout<<"threadFunc end. ID : "<<GetCurrentThreadId()<<std::endl;
    return 0;
}
void main()
{
    unsigned int threadID = 0;
    // 초기화
    InitializeCriticalSection(&cri);
    // 스레드 생성
    HANDLE hThread1 = (HANDLE)_beginthreadex(nullptr, 0, threadFunc, nullptr,
        CREATE_SUSPENDED, &threadID);
    std::cout<<"create thread ID : "<<threadID<<std::endl;
    HANDLE hThread2 = (HANDLE)_beginthreadex(nullptr,0,threadFunc, nullptr,
        CREATE_SUSPENDED, &threadID);
    std::cout<<"create thread ID : "<<threadID<<std::endl;
    HANDLE hThread3 = (HANDLE)_beginthreadex(nullptr,0,threadFunc, nullptr,
        CREATE_SUSPENDED, &threadID);
    std::cout<<"create thread ID : "<<threadID<<std::endl;
    // 널 체크
    if (hThread1 == nullptr || hThread2 == nullptr || hThread3 == nullptr)
    {
        std::cout<<"error."<<std::endl;
        return;
    }
    // 종료
    ResumeThread(hThread1);
    ResumeThread(hThread2);
    ResumeThread(hThread3);
    WaitForSingleObject(hThread1, INFINITE);
    WaitForSingleObject(hThread2, INFINITE);
    WaitForSingleObject(hThread3, INFINITE);
    CloseHandle(hThread1);
    CloseHandle(hThread2);
    CloseHandle(hThread3);
    return;
}
 
cs


커널모드 동기화

다른 프로세스들의 스레드간에도 동기화가 가능하다.

이벤트(Event, Mutex, Semaphore)가 있다.


커널 객체는 신호받음(Signaled)상태와 신호 못 받음(Non-Signaled)상태 중 하나를 가진다.

이 커널 객체의 상태는 핸들(Handle)을 통해 알 수 있다.


커널 객체 종류 

Signaled 

Non-Signaled 

프로세스 

프로세스가 종료 되었을 때 

프로세스가 종료되지 않고 계속 살아 있을 때 

스레드

스레드가 종료 되었을 때 

스레드가 종료되지 않고 계속 살아 있을 때 

이벤트

PulseEvent(), SetEvent() 함수 호출이 되었을 때 

ResetEvent() 함수 호출이 되었을 때, 자동리셋 모드 이벤트는 신호 받음 상태 후에 자동으로 신호 못 받음 상태로 바뀔 때 

뮤택스 

뮤택스 객체를 소유한 스레드가 존재하지 않을 때 

뮤택스 객체를 소유한 스레드가 존재할 때 

세마포어 

세마포어 객체의 내부 카운트가 0이상일 때 

세마포어 객체의 내부 카운트가 0일 때 


커널 객체의 상태가 중요한 이유 : 어떤 작업을 하는데 커널 객체의 상태를 보고 대기, 수행을 결정할 수 있다.


1
DWORD WaitForSingleObject(HANDLE hHandle, DWORD dwMilliseconds);
cs


WaitforSingleObject() 함수는 설정해 놓은 시간 동안 한 개의 커널 객체가 신호 받음 상태가 될 때까지 대기 상태로 만든다.

설정한 시간동안 신호 받음 상태가 않된다면 WAIT_TIMEOUT을 반환하여 대기 상태가 해제되고

신호 받음 상태가 된다면 WAIT_OBJECT_0을 반환하면서 대기 상태를 해제한다.


1
2
3
4
DWORD WaitForMultipleObject(
DWORD nCount, const HANDLE* lpHandles, 
BOOL bWaitAll, DWORD dwMilliseconds
);
cs


WaitForMultipleObject는 여러 개의 커널 객체들 중 하나 이상 or 모두 신호 받음 상태가 될 때까지 대기한다.

nConunt는 커널 객체의 개수, lpHandles는 커널 객체 핸들의 배열 시작 포인터,

bWiatAll은 TRUE면 모두, FALSE면 한개만 기다린다. 


이벤트

특정 스레드를 원하는 시점에 대기 상태에서 실행 상태로 바꿀 수 있다.

스레드의 작업 순서나 시기를 조정할 수 있다. 한 개 or 여러 개의 스레드를 다룰 수 있다. 

게임 서버 프로그램에서 가장 많이 쓰는 방식.


이벤트의 모드

자동 리셋(Auto-Reset) 모드

- 여러 개의 스레드가 동시에 같은 이벤트에 대기하고 있어도 한 개의 스레드만 실행 상태가 되고 나머지는 대기 상태.

- 대기하고 있는 여러 개의 스레드 중 어느 스레드가 실행 상태로 바뀔지는 알 수 없다.

- ResetEvent를 별도로 호출할 필요 없고 프로그래머가 SetEvent를 호출해 이벤트를 신호 받음 상태로 만들면 된다.


수동 리셋(Manual-Reset) 모드

- 신호 받음 상태가 되었을 때 자동으로 신호 못 받음 상태가 되지 않는다.

- 현재 대기 중인 모든 스레드가 깨어난다.

- ResetEvent를 별도로 호출해줘야한다.


CreateEvent() 함수는 이벤트 커널 객체를 생성하는 함수.

bManualReset - 수동 리셋 사용할 것인지, bInitialState - TRUE면 초기 상태를 신호 받음.


SetEvent() - 이벤트 객체를 신호 받음 상태로 바꾼다.

ResetEvent() - 지정된 이벤트 객체를 신호 못 받음 상태로 바꾼다.

PulseEvent() - 지정된 이벤트 객체를 신호 받음에서 바로 신호 못 받음으로 바꾼다.

728x90
반응형

'프로그래밍 > 서버' 카테고리의 다른 글

리눅스 네트워크 명령어  (0) 2015.08.10
vim 설치  (0) 2015.08.09
프라우드넷  (0) 2015.07.24
Thread  (0) 2015.07.23
Blocking vs Non-Blocking  (0) 2015.07.12
[서버] 데드 레커닝(Dead Reckoning)  (1) 2015.05.23
Overlapped I/O 모델  (0) 2015.05.17