2015. 7. 23. 02:55ㆍ프로그래밍/서버
동기화
스레드나 프로세스간에 공유자원의 접근을 제어하는 방법.
커널 객체
커널에서 관리되는 객체를 의미한다.
종류로는 파일객체, 이벤트 객체, 뮤텍스 객체, 스레드 객체, 세마포어 객체 등이 있다.
커널 객체의 특징
- 다른 객체와 접근하는 방법이 다르다. 실제로 생성하는 것은 커널이며 프로그래머는 요청을 하는 것이다.
메모리 공간에 객체를 할당하고 이를 식별하기 위한 '핸들'을 프로그래머에게 반환해준다.
- 소멸 시점이 다르다. 일반 객체는 프로세스가 소멸시 종료하지만
커널 객체는 공유하고 있는 프로세스가 있다면 (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() - 지정된 이벤트 객체를 신호 받음에서 바로 신호 못 받음으로 바꾼다.
'프로그래밍 > 서버' 카테고리의 다른 글
리눅스 네트워크 명령어 (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 |