CRITICAL_SECTION 사용법 및 문제점
우리는 프로그램을 작성할 때 다중 THREAD에서의 객체 접근을 보호 하기위한 방법으로 CRITICAL_SECTION을 많이 사용한다.
사용법은 아래와 같다 .
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
62
63
64
|
/******************************************************************************
* _ _ _ _ __ _____ _ _
*| | | | | | | |/ / | __ \ | | | |
*| |__| | __ _ ___ | |_ _| ' / | | | | _____ __ | | __ _| |__
*| __ |/ _` |/ _ \_ | | | | | < | | | |/ _ \ \ / / | | / _` | '_ \
*| | | | (_| | __/ |__| | |_| | . \ | |__| | __/\ V / | |___| (_| | |_) |
*|_| |_|\__,_|\___|\____/ \__,_|_|\_\ |_____/ \___| \_(_) |______\__,_|_.__/
*
* Copyright (c) HaeJuK Dev Lab All Rights Reserved.
*
*******************************************************************************/
/**
@file CriticalSectionTest.cpp
@brief
@author Woolim Choi
@date Create 2017.10.25
@ntoe 멀티쓰레드에서의 크리티컬섹션사용법
*/
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
UINT Foo();
UINT WINAP thread1I();
UINT WINAP thread2I();
CRITICAL_SECTION g_Cs;
UINT Foo()
{
::EnterCriticalSection(&g_Cs);
// ... ... ...
::LeaveCriticalSection(&g_Cs);
}
void main()
{
::InitializeCriticalSection(&g_Cs);
DWORD dThread1id = 0 , dThread2Id = 0 ;
HANDLE h1 = CreateThread(NULL,0,thread1(),NULL,0,&dThread1Id);
HANDLE h2 = CreateThread(NULL,0,thread2(),NULL,0,&dThread2Id);
//TODO: WaitForMultipleObjects()....
::CloseHanle(h1);
::CloseHanle(h2);
::DeleteCriticalSection(&g_Cs);
}
UINT WINAPI thread1()
{
return Foo();
}
UINT WINAPI thread2()
{
return Foo();
}
|
cs |
하지만 이 코드에는 무시무시한 문제점이 존재 한다. 함수의 리턴 값이 VOID 인것인다.
InitializeCriticalSection은 결국 커널 OBJECT의 HANDLE을 사용하는것이다.
사용자 메모리는 한정 적이다. 결국 사용자의 메모리가 FULL일 경우 즉 LOW 메모리 일 경우
분석하기도 어려운 에러가 발생한다.
아래는 SniperAaron (sniper209)님의 블로거 내용 입니다.
출처 : http://blog.naver.com/process3?Redirect=Log&logNo=20026509730
CriticalSection 은 vista 에서 데드락에 빠질 수 있습니다. 이 내용에 대해서 말씀 드리는 것은 아니에요
문제 발생 원인
EnterCriticalSection 의 API 를 MSDN 에서 확인 해 보면 리턴값이 VOID 형 입니다. 즉, 이 함수를 호출한
후에 성공했는지 실패했는지 확인 할 수가 없습니다. 그리고 일반적으로 우리가 사용 할 때도 리턴값을
확인하지 않습니다. 그런데 만약에 시스템에 메모리가 low memory situations 상태 일 때(가용메모리가
부족 할 때) EnterCriticalSection 함수 호출한 곳에서 Exception 이 발생 합니다. 만약에 이럴 경우에는
정말로 찾기 어려운 버그가 발생하는 것입니다.
문제 해결 방법
1) SEH (structured exception handling) 을 이용 한다.
즉, 예외 처리를 사용하는 것입니다. (SHE 의 자세한 내용은 "갱주니" 님의 블로그를 참조해 주세요.
http://blog.naver.com/process3/20023779161)
2) InitializeCriticalSection 초기화 함수 대신 InitializeCriticaSectionAndSpinCount 를 사용 한다.
기존에 우리가 사용하던 InitializeCriticalSection 함수 대신에 InitializeCriticalSectionAndSpinCount 를 사용하면 됩니다.
(InitializeCriticalSectionAndSpinCount 를 사용 하면 이벤트를 미리 할당해 놓고(Preallocate Event)
EnterCriticalSection 을 사용 하기 때문에 문제가 없습니다.)
스핀 카운터란?
스핀 카운트는 다중 프로세서 시스템에서 여러 프로그램이 동시에 같은 리소스를 엑세스 해야 할 때 성능을
높이기 위한 방법 입니다.
스핀 카운트는 프로세서가 기다리기 전에 리소스를 엑세스 하기 위해 시도하는 횟수를 제어 합니다.
명확한 이해를 위해, 서로 다른 프로세서에서 동시에 실행 중인 여러 쿼리를 가진 데이터베이스 응용 프로그램을 생각 해 보겠습니다.
쿼리 1번이 특정 테이블의 행에 쓰려고 하지만 쿼리 2번이 그 행을 잠궜기 때문에 엑세스가 거부 됩니다. 스핀 카운트가 없다면 쿼리 1번은 쿼리의 수행을 방해 하며 잠시 대기 해야 합니다. 스핀 카운트를 사용 하는 경우 쿼리 1번은 쿼리 2번이 행을 빨리 해제 할 것으로 기대 하면서 쓰기를 몇 회 시도 하면 되며, 모든 시도가 거부 될 경우에만 대기 합니다.
다중 프로세서 시스템에서는 여러 프로세스가 동시에 실행 할 수 있기 때문에 이는 다중 프로세서 시스템에서만 유용 합니다. 단일 프로세서 시스템에서 각 프로세스는 다른 프로세스가 어떤 식으로든 실행되기를 기다려야 하며, 그럴 경우 리소스에 대한 반복적인 엑세스 시도는 다른 프로세스가 실행 기회를 가질 때까지 성공 하지 못합니다.
단일 프로세서 시스템에서 스핀 카운트를 사용 하는 응용 프로그램은 성능이 저하 되지 않지만 얻는 이점도 없습니다.
출처 : http://hyurichel.tistory.com/entry/InitializeCriticalSectionAndSpinCount
EnterCriticalSection 에서는 시스템 메모리가 부족할 때 오류가 발생합니다.
이것을 처리해 주기 위해서 throw-catch를 이용하거나 InitializeCriticalSectionAndSpinCount로 초기화 하는 방법을 제시 했었습니다.
그렇다면 InitializeCriticalSectionAndSpinCount이 뭐냐!!?
msdn을 뒤져보니까
BOOL InitializeCriticalSectionAndSpinCount(
LPCRITICAL_SECTION lpCriticalSection, DWORD dwSpinCount);
Return. 성공하면 0이 아닌 값.
실패하면 0이 나옴. GetLastError로 에러를 살펴보면 되겠습니다.
이렇게 생겼습니다. 리턴값도 있고 기존의 것과 다른 인자가 하나 더 들어가는군요. 바로 dwSpinCount. Spin을 할 횟수를 지정하면 됩니다.
dwSpinCount는 싱글코어일 경우에 인자가 무시됩니다.(자동으로 0으로 셋팅됩니다)
멀티코어일 경우, 만약 Critical Section이 이용되지 않으면, Critical Section과 함께 세마포어 연합(?) 의 기다리는 동작이 Thread Spin을 부를 때 dwSpinCount만큼 부르게 됩니다.
만약 Spin동작에서 CriticalSection이 무효화 된다면 부르게 되는 Thread는 기다리는 동작을 피하게 됩니다.
...이게 뭔소리냐-_- 직독직해를 해버리니까 이따위로 나오는군요. 대충 의미를 되세겨 봅시다.
Spin은 Wait 상태로 들어가기 전에 함수의 인자로 주어진 dwSpinCount만큼 루프를 돈다는 의미입니다. 루프 도는 중간에 Critical Section을 획득할 수 있다면 Thread를 교체하는 (Thread Context Switch)가 발생하지 않고 임계영역안에 접근 가능하다는 의미. 검색을 해보니까 dwSpinCount를 2000정도로 주는 것이 좋다고 합니다.
예를 들어보자면 1번 쿼리와 2번 쿼리가 있다고 했을 때 2번 쿼리에서 어는 특정 테이블을 변경하고 있다고 해 봅시다. 그런데 1번 쿼리에서 2번 쿼리가 변경하고 있는 테이블 값에 접근을 하려고 하는 경우 Critical Section에 의해서 접근이 불가됩니다. 이 때 일반적인 CriticalSection이라면 대기상태로 돌입하게 되는데, SpinCount가 지정되어 있다면 2번이 빠리 끝나기를 바라며 dwSpinCount만큼 재접근을 하는 겁니다. 그리고 실패해버리면 기다리는 모드로 들어가게 되고 성공하면 접근하는 것입니다.
이렇게 되면 싱글코어에서는 별 의미없겠지만 다중 프로세서일 경우에는 많은 의미가 생기겠지요? Thread의 ContextSwitch에 부하가 있는 데 그것을 피할 수 있을 가능성이 높아지겠지요. 기다리는 모드로 돌입하기 전에 접근 가능하는 경우가 꽤 많을 테니까요. 이상.