본문 바로가기
프로그램 (PHP,Python)

쓰레드(Thread)간의 동기화(Synchronization)

by 날으는물고기 2009. 1. 30.

쓰레드(Thread)간의 동기화(Synchronization)

멀티 태스킹을 말할때 피할수 없는것이 동기화(Synchronization)문제이다.

동기화란 멀티 태스킹 환경에서 여러개의 처리(태스크)를 서로의 진행상태에 맞추어 진행시키는것을 말한다.

문제점 코드 (플래그를 사용한 배타적 제어 코드)

bool InUse = false;

int func(void)

{

....

while(InUse)

{

 

}

InUse = true; // 다른곳에서 접근 금지

File * fp = fopen("sample.dat", "wb");

if(fp != NULL)

{

  ......

fclose(fp);

}

InUse = false; // 다른곳에서 접근 허용

}

이 코드의 문제점은 첫째, InUse가 false인것을 확인하는 조작가 true로 조작 하는 사이에 비록 짧지만 시간간격이잇다.

이처럼 얼마되지 않은 시간에 다른 쓰레드가 사용가능하다고 판단해서 리소스 접근이 진행될수있다.

비록 CPU가 하나밖에 없어도 어떤 쓰레드가  InUse를 체크한후에 다른쓰레드로 바뀌고 거기서 또다시 InUse가 체크될 가능성이 있다. 시간간격이 짧아지면 가능성은 줄어들지만 이러한 문제점을 내포할수가 있다.

왜냐하면 Windows에서는 어플리케이션 수준에서 쓰레드 전환을 금지할수 없기때문에 이문제를 막을수가 없다.
 

또다른 문제는 InUse가 false가 될때까지 대기하는 루프이다. 이 코드를 실행하면 InUse가 true인동안에는 고속으로 루프를 반복한다.

단지 대기만 하는것에 불과한테 쓸데없이 CPU를 소비하여 다른 프로그램이나 쓰레드에 영향을 미치게 된다.

이런루프를 비지웨이트(busy-wait)라고 하며 멀티쓰레드에서는 최대한 피해야 한다고 알려져있다.

유감스럽게도 C/C++언어수준에서는 이런문제를 해결할수 없다. 즉 동기화를 수행하기위해서는 Windows등 운영체제에서 지원이 필요하다.

크리티컬 섹션 자체는 Windows객체중에 하나며 프로그램에서 CRITICAL_SECTION 변수를 하나 선언해서 사용한다.

일반 Windwos 객체와 달리 프로세스 메모리 공간에 확보된 변수를 이용하기 때문에 동일 프로세스내의 쓰레드 동기화에 사용할수 있지만 다른 프로세스 동기화에는 사용할수없다.

객체를 사용해서 배타적제어를 한다는 말에 감이 오지 않은 사람도 잇을것이다.

쉽게 말해서 객체의 소유권이라고 생각하면 된다. 객체를 소유하는것은 쓰레드이다.

배타적 제어를 하려면 공유리소스에대해 크리티컬 섹션을 정의한뒤, 접근하고싶은 스레드는 우선 그 객체의 소유권을 시스템에게 요구한다.

소유권을 얻은 스레드는 리소스에 접근하고 그후에 소유권을 반납한다.

소유권은 하나뿐이므로 리소스에 접근할수 있는 쓰레드 역시 하나뿐이다.

크리티컬섹션을 사용할 경우에는 공유리소스에 접근 하는 부분의 코드를 정확히 EnterCriticalSection API와 LeaveCriticalSection API로 둘러싸는 형태가 된다. 둘러싸인 코드의 접근시 쓰레드간 경합을 일으킬 가능성이 있는 위험한 구역이므로 말그대로 크리티컬 섹션인것이다.

다른쓰레드에 소유권이 있어 획득할수없는 경우 EnterCriticalSection을 호출한 쓰레드는 대기상태로 들어가서 돌아오지 않는다.

그리고 소유권을 획득할수 잇는 상태가 되면 Windows는 자동적으로 쓰레드 실행을 재개해준다.

대기중인상태에서는 CPU파워를 소비하지 않기때문에 앞서와 같은 busyWait 문제가 발생하지 않는다.

EnterCriticalSection 대신 TryCriticalSection을 사용하면 소유권 획득여부와 관계없이 곧바로 복귀한다. 즉 이를 이용하면 소유권을 획득할수 잇을때까지 다른처리를 계속 할수있다.

CRITICAL_SECTION critisec; 

int func(void)

{

EnterCriticalSection(&critisec);

File * fp = fopen("sample.dat", "wb");

if(fp != NULL)

{

  ......

fclose(fp);

}

LeaveCriticalSection(&critisec);

}

int main(void)

{

InitializeCriticalSection(&critisec);

// 쓰레드 생성

DeleteCriticalSection(&critisec);

return 0;

}

주의 해야할점은 보호해야할 리소스와 크리티컬섹션 사이에는 시스템과 아무런 관련이 없다는 점이다.

이는 예제에서 API를 호출할때 파일포인터나 파일명을 넘기지 않는 것을 보면 분명하게 알수있다.

따라서 위험영역을 올바르게 싸지 않아도 API 호출은 에러가 발생하지않는다.

다만 실행시에 쓰레드간 경합때문에 오동작 하는경우가 잇을 뿐이다. 이는 모든 동기화구조에서 공통된 점이다.

동기화 매커니즘을 올바르게 집어 넣는것도 프로그래머의 책임이다.

======================================================================================================================

크리티컬 섹션도 동기화 객체이지만 약간 구조가 달라서 구별되는 동기화 객체가 있다.

다음은 Windows에서 제공하는 주요 동기화 객체이다.

           동기화개체                      용도

       1) 뮤텍스                            리소스 배타적 제어

       2) 세마포어                         리소스를 동시에 사용할수 있는 개수 제어

       3) 이벤트                            다른 쓰레드에 이벤트 통지

       4) 대기가능 타이머               타이머 이벤트 통지

크리티컬 섹션과의 큰 차이는 동기화 객체는 모두 프로세스 간의 동기화에 사용 할수 있다는 점이다.

이는 크리티컬 섹션 객체와 달리 동기화 객체의 실체가 Windows 내부에 확보되어 잇음을 의미한다.

프로그램에서는 그 핸들을 사용해 객체를 조작한다. 따라서 핸들을 얻을수만 있으면 어느 프로세스에서도 동기화 객체에 접근할수가

있다. 핸들 그자체를 프로세스 간에 주고받는것이 귀찮기는 하지만 (핸들값은 프로세스 내부에서만 유호하기때문에 다른프로세스에 값을 그냥 넘기면 이용할수없다) 동기화 객체에는 파일처럼 이름을 붙일수 잇으므로 이를 지정해서 동기화 객체의 핸들을 얻으면 된다.

또한 동기화객체는 객체의 시그널 비시그널 상태라는 속성을 이용해 동기를 얻는다는점이 다르다.

WaitForSingleObject / WaitForMultipleObject API를 이용하여시그널상태가 되는것을 감시한다.

뮤텍스는 Windows객체이므로 필요 없으면 삭제하는 편이 바람직하다. 이때는 파일객체나 프로세스 객체처럼 CloseHandle API를 사용해서 닫으면 된다. 단 닫아도 즉석에서 객체가 삭제되는 것은 아니라는 사실에 주의 해야한다.

동기화 객체는 여러개의 프로세스나 쓰레드에서 접근할 가능성이 있기 때문에 어디선가 닫았다고 해서 즉석에서 삭제되어야 한다면 나머지 프로세스 쓰레드에게는 곤란한 일이다. 그러므로 모든 핸들이 닫혓을때만 실제로 삭제된다.

이를 뒤집어 생각해보면 하나라도 닫히지 않은 핸들이 잇다는 말은 객체가 삭제되지않고 남는다는 사실을 의미한다.

이는 리소스가 낭비되므로 필요없다면 즉시 닫는 버릇을 들이자. 또한 파일이나 다른 Windows객체처럼 프로세스가 종료되면 사용중이던 핸들이 자동으로 닫힌다.

HANDLE hMutex;

int func(void)

{

WaitForSingleObject(hMutext, INFINITE);

FILE *fp= fOpen("Sample.dat","wb");

if(fp != NULL)

{

.....

fclose(fp);

}

ReleaseMutex(hMutex);

}

int main(void)

{

hMutex = CreateMutex(NULL, FALSE, "Sample_mutex");

 

//스레드 생성해서 func 호출

 

// 뮤텍스 닫기

CloseHandle(hMutex);

return 0;

}

유한개의 리소스 관리에는 세마포어를 사용한다.

세마포어는 뮤텍스와 비슷하지만 카운터를 관리한다는 점이 다르다.

세마포어는 관리하는 카운터값이 0이면 비시그날상태 1이상이면 시그날상태가된다. 대기용 API는 시그널상태에서 실행을 재개하는 동시에 세마포어의 카운트값은 1씩 줄어든다. 카운트가 0이되면 세마포어는 다시 비시그날상태로된다.

그리고 ReleaseSemaphore API로 카운터를 증가하면 다시 세마포어는 시그날상태로 된다.

이러한 세마포어를 이용하는 경우는 어떤인위적인 제한을 하고 싶을때 이용한다.

예를들어 성능을 확보하거나 메모리 사용량을 억제하기위해 데이타베이스나 웹서버에 접근하는 클라이언트수를 제한하고자 할때 이용하는 경우가잇다.

마지막으로 소개할 이벤트 객체는 뮤텍스나 세마포어와는 성잘이 좀 다르다. 소유권이나 카운터같은 리소스 접근 제어 속성이 없다.

그 대신 객체의 시그널상태를 프로그램에서 자유롭게 제어할수잇다. 즉 대기용API를 호출해서 정지한 스레드에대한 이벤트 객체를 시그널상태로 바꾸는 방법으로 실행을 재개 할수있다. 이 기능을 재개를 위한 이벤트 통지라고 생각해도 괜찮을것이다.


출처 : http://blog.naver.com/kkan22

728x90

댓글