같은 인스턴스에 접근하는 모든 쓰레드는 해당 클래스의 멤버 변수로 선언된 값을 공유하기 때문에 멀티 쓰레드 애플리케이션 작성시 항상 공유 자원 보호 문제가 대두되기 마련이다. 즉 다수의 쓰레드가 동시에 한 인스턴스를 접근할 경우 해당 인스턴스의 멤버 변수에 저장되어 있는 값을 어떻게 thread-safe하게 관리할 것인가하는 문제가 발생하게 된다. 닷넷에서는 다양한 방법을 통해 동기화 문제를 유발할 수 있는 코드에의 접근을 serialize시킨다. 즉, 한번에 한 쓰레드(mutual exclusion)만이 해당 코드 블럭을 수행할 수 있게 바꾸는 방식으로 이 문제에 대한 답을 제공하고 있다.

1. Interlocked 클래스와 Monitor 클래스
한 프로세스내의 다수의 쓰레드 사이의 동기화 문제를 해결하기 위해 닷넷 이전부터 제공되어 오던 Win32 API를 닷넷버전으로 매핑한 경우이다. Interlocked 클래스는 Win32 API에서 제공하는 많은 메소드를 묶어 static 메소드로 노출하는 닷넷 클래스이다. Interlocked.Increment, Interlocked.Decrement, Interlocked.Exchange 등의 메소드를 가지고 있다. Monitor 클래스는 Win32 API의 CRITICAL_SECTION 구조체를 다루는 메소드를 닷넷 버전으로 모아 놓은 것이다. Win32 API의 EnterCriticalSection은 Monitor.Enter()로, LeaveCriticalSection은 Monitor.Exit()으로 형상화되었다.
Interlocked 클래스와 Monitor 클래스 중에 Interlocked를 쓰는 것이 성능상의 잇점이 있다. 즉 좀 더 가볍다.
2. lock 키워드
가장 일반적이면서 쉽게 쓰레드 동기화 문제를 해결할 수 있는 키워드가 lock이다. 공유자원에 대한 접근 중에 동기화 문제를 유발할 수 있는 블럭을 선택하여 lock을 통해 여러 쓰레드의 동시 접근을 막고 한번에 하나씩 수행을 하도록 하는 기능, 즉 mutual exclusion을 제공한다. lock은  컴파일 시점에 위에서 언급한 Monitor.Enter()와 Monitor.Exit()을 통해 코드가 재 해석되어 변경된다. 즉, lock()은 Monitor 클래스를 통해 구현된다.
private Object theLock = new Object();
lock (theLock) {
.....
}
3. ReaderWriterLock / ReaderWriterLockSlim
Read를 위한 쓰레드인지, Write를 위한 쓰레드인지를 구분하여 다수의 쓰레드가 Read만을 원할 경우 동시에 데이터에 접근하도록 허용하되, Write를 원하는 쓰레드가 작업중이면 모든 Read를 원하는 쓰레드는 대기상태에 들어가게 되는 원리이다. Reader를 위한 대기큐와 Writer를 위한 대기큐를 두어 좀 더 효과적인 리소스 활용이 가능하다. ReaderWriterLock클래스가 성능상의 문제가 있어서 .NET Framework 3.5부터 ReaderWriterLockSlim 클래스를 별도로 제공하는데, 가급적 언제나 새로 제공되는  ReaderWriterLockSlim을 쓰도록 한다.
ReaderWriterLockSlim theLock = new ReaderWriterLockSlim();
theLock.EnterReadLock() / theLock.ExitReadLock()
theLock.EnterWriteLock() / theLock.ExitWriteLock() 등의 메소드를 이용한다.
4. Mutex
앞에 열거한 기법들이 하나의 프로세스내의 다수 쓰레드 사이의 공유 자원 동기화를 위한 장치들이라면, Mutex는 이들과는 약간 다르게 여러 프로세스들 사이의 공유 자원 접근 문제를 해결하기 위한 장치이다.
5. Semaphore
Semaphore는 앞의 기법들이 한 프로세스 내의 쓰레드 동기화이거나 혹은 여러 프로세스 내의 쓰레드 동기화건 mutual exclusion 기반의 동기화 장치인데 반해 Semaphore는 동기화 관련된 코드 블럭에 접근 가능한 쓰레드의 수를 일정하게 유지하는 기법이다. 즉, 여러 쓰레드가 동시에 접근 가능하다. 아래는 최대 5개의 쓰레드가 동시 접근 가능한 semaphore를 설정하는 것이다.
private Semaphore semaphore = new Semaphore(0, 5);
semaphore.WaitOne();
try {
....
} finally {
  semaphore.Release();
}

lock(), Monitor, Interlocked 등이 사용자 모드에서 쓰레드 동기화를 처리하는 것인데 반해, Mutex와 Semaphore는 OS레벨의 커널 모드 객체들을 통해 구현되므로 훨씬 무겁기 때문에 가능하다면 언제나 좀더 가벼운 기능을 사용하도록 하는 것이 필요하다.

Posted by 장현춘

댓글을 달아 주세요

  1. Favicon of http://rhea.pe.kr/ BlogIcon Rhea君 2008.08.21 00:23  댓글주소  수정/삭제  댓글쓰기

    안녕하세요?
    공부를 하다보니 Event는 커널개체이기 때문에 가장 빠르다(혹은 가볍다) or Critical Section은 단순한 알고리즘 기반으이므로 가장 빠르다(혹은 가볍다)라고 상반된 내용들을 접합니다.
    물론 전체 수행에서 각기 다르겠지만 단순히 둘만 놓고 본다면 어느 것이 정답일까요?;; 궁금합니다. ^^ (물론 제 질문 자체가 잘못되었을수도 있다고도 생각합니다.)

  2. 장현춘 2008.08.21 10:08  댓글주소  수정/삭제  댓글쓰기

    안녕하세요. 갑자기, 빛처럼 빠른 100 메가급..하는 광고가 생각나는군요.. ^^
    말씀하신 Event와 Critical Section에 관한 사항이 여기와 좀 다른 맥락에서 보셨을 것 같네요. C#과 같은 managed 환경에서 커널객체 기반의 Event를 쓰면 사용자 모드에서 커널모드로의 transition이 일어나 느리겠지만, unmanged 환경에서 커널 객체를 직접 핸들링하는 차원이라면 다른 얘기가 아닐까 합니다.
    또한 위에서 정리한 것은 같은 프로세스내 혹은 다른 프로세스의 쓰레드 동기화 매커니즘이며, Event는 이중에서 프로세스 사이의 동기화에 필요한 이벤트 발생에 관여하는 것으로 생각됩니다.
    말씀하신 unmanaged 환경에서 단순한 두 녀석의 속도 비교는 .. 잘 모르겠습니다. 지나가다 아시는 분이 답변을 달아주시길...

    대학때 좀 공부좀 할껄 하는 생각이 마구 듭니다. --;