System.Object System.MarshalByRefObject System.Threading.WaitHandle System.Threading.AutoResetEvent System.Threading.ManualResetEvent System.Threading.Mutex위에서 알 수 있는 것처럼 닷넷 동기화 객체는 WaitHandle 기본 클래스를 상속한 AutoResetEvent와 ManualResetEvent, Mutex 클래스가 있으며, 이들 각각의 동기화 객체는 실행시간에 WaitHandle 객체로 대표해서 처리할 수 있다는 것을 알 수 있다. 또한, WaitHandle 클래스는 Win32 동기화 핸들을 캡슐화하고 있다. AutoResetEvent와 ManualResetEvent는 쓰레드 동기화를 위해 이벤트를 사용하는 경우에 사용한다.
// 임계 영역을 선언한다. CRITICAL_SECTION cs; // 임계 영역을 초기화한다. InitializeCriticalSection(&cs); // 임계 영역으로 들어간다. 여기서는 한 번에 하나의 쓰레드만 // 임계 영역으로 들어갈 수 있으며, 이미 임계 영역에 들어간 // 쓰레드가 있으면 다른 쓰레드는 여기서 대기한다. EnterCriticalSection(&cs); try { // 한 번에 하나의 쓰레드에 의해서만 실행될 수 있는 코드 블록이다. } finally { LeaveCriticalSection(&cs); // 임계 영역을 빠져나온다. } DeleteCriticalSection(&cs); // 임계 영역을 정리한다.닷넷에서의 임계 영역 - Monitor
Monitor.Enter(this); // 임계 영역을 시작한다 Monitor.Exit(this); // 임계 영역을 종료한다닷넷에서 임계 영역을 사용하는 예제를 살펴보도록 하자.
이름 : CritSec01.cs using System; using System.Threading; public class AppMain { private int counter = 0; public static void Main() { AppMain app = new AppMain(); app.DoTest(); } public void DoTest() { Thread t1 = new Thread( new ThreadStart(Incrementer) ); Thread t2 = new Thread( new ThreadStart(Decrementer) ); t1.Start(); t2.Start(); } private void Incrementer() { Monitor.Enter(this); try { while ( counter < 10 ) { Console.WriteLine("Incrementer : " + counter.ToString()); counter++; } } // end of try finally { Monitor.Exit(this); } // end of finally } // end of Incrementer private void Decrementer() { Monitor.Enter(this); try { while ( counter > 0 ) { Console.WriteLine("Decrementer : " + counter.ToString()); counter--; } } // end of try finally { Monitor.Exit(this); } // end of finally } // end of Decrementer }위 예제에서 알 수 있는 것처럼 두 개의 쓰레드를 생성하고, 각각의 쓰레드는 counter의 값을 조건에 따라 증가시키거나 감소시킨다. 여기서 counter는 두 쓰레드가 공유하는 공유 데이터가 된다. 따라서 이 값을 변경하는 코드 부분을 Monitor 클래스를 사용하여 임계 영역으로 선언하고 있다. 코드를 컴파일하고 실행하면 결과는 다음과 같을 것이다.
하나의 쓰레드가 이미 임계 영역을 사용중일 때 다른 쓰레드가 임계 영역을 사용하려면 임계 영역을 사용할 수 있게 될 때 까지 기다려야다. 만약에 쓰레드가 기다리는 것을 것을 원하지 않는다면 Monitor.Enter() 대신에 Monitor.TryEnter()를 사용하도록 한다.
Monitor.Enter(this);를 모두 다음과 같이 변경한다.
Monitor.TryEnter(this);다시 컴파일하여 실행된 결과는 다음과 같다.
결과에서 볼 수 있는 것처럼 Decrementer 쓰레드의 첫번째 출력은 7이며, 다음 출력은 9가 되는 것을 알 수 있을 것이다.(여러 번 실행해야 위와 같은 결과를 볼 수 있으며, 실행할때마다 약간씩 다른 결과가 나타나는 것을 볼 수 있을 것이다) 위에서 설명한 것처럼 Monitor.TryEnter()는 임계 영역에 들어갔는지와 관계없이 코드를 실행한다. 따라서 Monitor.TryEnter()를 사용하려면 다음과 같이 수정해야한다. 여기서는 Decrementer()만을 수정했다.
private void Decrementer() { // 임계 영역에 들어가지 못했다. if ( Monitor.TryEnter(this) == false ) { Console.WriteLine("임계 영역에 들어가는데 실패"); } // 임계 영역에 들어간 경우 else { while ( counter > 0 ) { Console.WriteLine("Decrementer : " + counter.ToString()); counter--; } Monitor.Exit(this); } } // end of Decrementer닷넷에서의 임계 영역 - lock()
private void Incrementer() { lock(this) { while ( counter < 10 ) { Console.WriteLine("Incrementer : " + counter.ToString()); counter++; } } // end of lock } // end of Incrementer private void Decrementer() { lock(this) { while ( counter > 0 ) { Console.WriteLine("Decrementer : " + counter.ToString()); counter--; } } // end of lock } // end of Decrementer컴파일하여 실행하면 결과가 같다는 것을 알 수 있을 것이다. 실제로 lock()은 Monitor.Enter()와 Monitor.Exit()의 역할을 수행한다. Monitor 클래스보다 lock()이 더 사용하기 간편하다는 것을 알 수 있을 것이다. lock()은 특정 코드 블록을 잠금으로서 동기화하는데 유용하며, Monitor 클래스는 lock()보다 공유 자원에 대해서 정교한 제어가 필요할 때 사용한다. 그렇다면 이러한 Monitor 클래스와 lock()은 어떻게 구현된 것일까? 필자는 Monitor 클래스와 lock()을 C++로 구현하였다.
이름 : SyncHandle.h #includehObject는 객체에 대한 핸들을 갖고 있으며, 생성자와 파괴자를 정의하고, 임계 영역에 들어가는 enter와 exit를 구현하고 있다. 여기서는 간단히 하기 위해 enter에 대해서는 오버로딩을 구현하지 않았다. 독자중에 System.Threading.Monitor 클래스 멤버들을 MSDN에서 찾아보았다면 Monitor.Enter()가 여러가지 버전으로 오버로딩되어 있다는 것을 알 수 있을 것이다. 이에 대해서는 다음에 다룰 것이다.class SyncHandle { protected: HANDLE hObject; // Monitor 객체에 대한 핸들 public: SyncHandle::SyncHandle(); SyncHandle::~SyncHandle(); virtual bool enter(DWORD dwTimeOut = INFINITE); virtual bool exit() = 0; };
이름 : SyncHandle.cpp #include "SyncHandle.h" SyncHandle::SyncHandle() { hObject = NULL; } SyncHandle::~SyncHandle() { if ( NULL != hObject ) { ::CloseHandle(hObject); hObject = NULL; } } bool SyncHandle::enter(DWORD dwTimeOut) { if ( WaitForSingleObject(hObject, dwTimeOut) == WAIT_OBJECT_0) return true; else return false; }파괴자에서는 객체에 대한 핸들이 있는지 확인하며, 핸들을 갖고 있다면 핸들을 정리한다. enter는 실제로 임계 영역에 들어가는 것이다. enter의 실제 구현은 WaitForSingleObject로 되어 있는데 하나의 객체에 대해서만 임계 영역에 들어갈 때까지 기다린다는 것을 의미한다. enter의 선언부분을 보면 virtual bool enter(DWORD dwTimeOut = INFINITE)이므로, 임계 영역을 획득할 때까지 무한히(INFINITE) 기다린다는 것을 뜻한다. 다시말해 Monitor.Enter()와 동일한 동작을 한다. WAIT_OBJECT_0는 대기동작이 성공한 것을 뜻한다.(대기동작의 실패는 WAIT_FAILED를 사용한다)
이름 : Monitor.h #includeMonitor 클래스의 선언파일은 SyncHandle에서 상속한 enter와 exit에 대한 선언 뿐만 아니라 임계 영역에서 사용할 수 있는 tryEnter를 추가로 구현한다. 또한 임계 영역 cs를 선언하고 있는 것에 주의한다.#include "SyncHandle.h" class Monitor : public SyncHandle { CRITICAL_SECTION cs; public: Monitor(); ~Monitor(); virtual bool enter(DWORD dwTimeOut = INFINITE); virtual bool exit(); bool tryEnter(); };
이름 : Monitor.cpp #include "Monitor.h" Monitor::Monitor() { // 임계 영역을 초기화한다 InitializeCriticalSection(&cs); } Monitor::~Monitor() { // 임계 영역을 정리한다 DeleteCriticalSection(&cs); } bool Monitor::enter(DWORD /*not used*/) { // 임계 영역에 들어간다. EnterCriticalSection(&cs); return true; } bool Monitor::exit() { // 임계 영역에서 빠져나온다 LeaveCriticalSection(&cs); return true; } bool Monitor::tryEnter() { #if( _WIN32_WINNT >= 0x0400 ) if ( TryEnterCriticalSection(&cs) ) return true; else #endif return false; }Monitor 클래스는 .NET의 Monitor 클래스와 큰 차이를 느끼지 못할 것이다. .NET에서 사용했던 Monitor.Enter(), Monitor.Exit(), Monitor.TryEnter()를 구현한 것이다. 실제로 임계 영역이 사용중일 때 대기하지 않도록 하려면 TryEnterCriticalSecion() Win32 API를 사용한다. 이 API의 동작은 Monitor.TryEnter()와 동일하다. #if는 C++에서 처리하는 전처리기이며, _WIN32_WINNT >= 0x0400은 TryEnterCriticalSecion() Win32 API가 윈도우 NT 4.0 이상에서만 동작하기 때문에 사용한 것이다. 윈도우 9x 계열에서는 이 API가 동작하지 않는다. 참고로 윈도우 2000은 _WIN32_WINNT 값이 0x0500이며, 윈도우 XP, .NET Server는 0x0501이다.(필자의 예상이 맞다면 닷넷에서 Monitor.TryEnter()는 윈도우 9x 계열과 NT 3.5이하에서는 동작하지 않을 것이다)
이름 : CritSec01.cpp #include코드를 모두 입력했다면 저장한다. 지금까지 총 5개의 C++ 파일을 작성하였다: SyncHandle.h, SyncHandle.cpp, Monitor.h, Monitor.cpp, CritSec01.cpp#include #include "Monitor.h" using namespace std; // 쓰레드 함수의 원형 DWORD WINAPI incrementer(LPVOID pv); DWORD WINAPI decrementer(LPVOID pv); Monitor monitor; int counter = 0; int main() { char* ps[] = {"incrementer", "decrementer"}; DWORD threadID; const int nThread = 2; HANDLE hThreads[nThread]; hThreads[0] = CreateThread( NULL, 0, incrementer, (LPVOID)ps[0], 0, &threadID); hThreads[1] = CreateThread( NULL, 0, decrementer, (LPVOID)ps[1], 0, &threadID); // 모든 쓰레드가 종료할 때 까지 기다린다. //DWORD rc = WaitForMultipleObjects(nThread, hThreads, TRUE, INFINITE); CloseHandle(hThreads[0]); CloseHandle(hThreads[1]); return 0; } DWORD WINAPI incrementer(LPVOID pv) { monitor.enter(); char* ps = reinterpret_cast (pv); while ( counter < 10 ) { counter++; cout << ps << " : " << counter << endl; } monitor.exit(); return 0; } DWORD WINAPI decrementer(LPVOID pv) { monitor.enter(); char* ps = reinterpret_cast (pv); while ( counter > 0 ) { counter--; cout << ps << " : " << counter << endl; } monitor.exit(); return 0; }
bcc32 -c SyncHandle.cpp bcc32 -c Monitor.cpp bcc32 CritSec01.cpp SyncHandle.obj Monitor.objVisual C++을 사용하고 있다면 다음과 같이 컴파일한다.
cl /c SyncHandle.cpp cl /c Monitor.cpp cl /EHs CritSec01.cpp SyncHandle.obj Monitor.obj두 컴파일러 모두 c 옵션은 C++ 소스 파일을 실행파일로 만들지말고 기계어 파일(.obj)로 만들라는 것을 뜻한다. VC++은 몇가지 차이점이 있기 때문에 예외가 발생하면서 컴파일되기 때문에 예외를 화면에 출력하지 않도록 하기 위해 EHs 옵션을 사용하였다. 실행 결과는 다음과 같다.
결과에서 알 수 있는 것처럼 닷넷에서 했던것과 Win32 API를 사용하여 직접 Monitor 클래스를 구현한 것과 큰 차이가 없다는 것을 알 수 있을 것이다. 여기서는 일부러 Win32 API를 사용하여 Monitor 클래스를 직접 구현했다. 임계 영역이 무엇인지, Monitor 클래스가 어떻게 동작하는지 알고 있다면 문제가 생겼을 때 문제의 원인을 하나씩 풀어가기 쉬운 것은 당연할 것이다. 마지막으로 글이 길어지면 한빛미디어 편집진에게 혼나기 때문에 닷넷의 lock()에 대한 구현은 여기서 소개하지 않고, 소스에만 포함시켜 두었다. 관심있는 분들은 Lock.h, Lock.cpp, CritSec02.cpp를 살펴보기 바란다. C#에서의 lock()을 C++에서는 Lock l(&cs);와 같이 사용한다는 정도의 차이만이 있을 뿐이다.(Lock 클래스는 Lock.h에 정의되어 있다) 이 소스를 곰곰히 분석해보면 닷넷에서 lock()이 어떻게 임계 영역으로 설정될 수 있는 범위를 설정하고(Enter) 나올 수 있는지(Exit) 알 수 있을 것이다.
이전 글 : LDAP로 시작하기
다음 글 : C# 쓰레드 이야기: 10. 뮤텍스(Mutex)
최신 콘텐츠