메뉴 바로가기 검색 및 카테고리 바로가기 본문 바로가기

한빛출판네트워크

IT/모바일

C# 쓰레드 이야기 - 14. 마지막 이야기

한빛미디어

|

2002-06-24

|

by HANBIT

46,263

저자: 한동훈(traxacun@unitel.co.kr)

지난 시간까지 쓰레드와 다중 쓰레드 프로그래밍에 대한 기초적인 설명들은 충분히 해왔다고 생각한다. 이번 시간에는 그동안 설명하지 못했던 부분들과 미흡했던 부분들, 그리고 쓰레드에 대해서 생각해 보아야 할 문제점을 비롯하여 닷넷에서 제공하는 쓰레드 프로그래밍의 문제점과 앞으로의 방향에 대해 함께 생각해보도록 하자.

교착상태의 조건

교착 상태는 아래와 같은 4가지 경우에만 발생할 수 있다.
  • 여러 리소스들의 소유와 대기
  • 상호 배제(Mutual Exclusion)
  • 무선점(No Preemption)
  • 순환 대기(Circular wait)
그렇지만 두 번째와 세 번째는 운영체제에서 보장해 주는 사항이기 때문에 신경쓰지 않아도 된다. 흔히들 윈도우가 처음 나왔을 때 무선점 운영 체제이냐 아니냐에 대한 이야기가 많았다는 것을 기억하는 분들이라면 그 "논쟁"이 바로 여기서 말하는 무선점이다. 윈도우 뿐만 아니라 다른 대부분의 운영체제들도 마찬가지로 상호 배제와 무선점을 보장해준다. 결국 프로그래머가 책임질 부분은 첫번째와 네 번째가 되며, 지금까지 이러한 자원들에 대한 베타적인 접근을 허용하기 위해 임계 영역, 뮤텍스, 잠금(lock)에 대해 살펴보았다. 이러한 교착 상태를 해결하는 방법은 여러 가지가 있지만 이중에서 대표적인 것을 나열하면 다음과 같다.
  • Detect
  • Prevent
  • Ignore
이중에서 세 번째 "무시한다(ignore)"는 실제로 운영체제에서 사용하는 방법이며, "검출한다(Detect)"는 부하가 많이 걸리기 때문에 실제로 잘 사용하지는 않지만 디버깅과 크리티컬한 서버 시스템을 프로그래밍하는 경우에 주로 사용한다. 실제로 이 방법을 구현하려면 우리가 사용하고 있는 x86 프로세서의 하드웨어적인 구조를 잘 알고 있어야 하며, 어셈블리에 대한 지식과 x86 op-code에 대한 지식도 필요하다. 따라서 일반적으로 프로그래머들은 "예방한다(Prevent)"를 주로 사용하고 있으며, 지금까지 설명해왔던 방법들은 교착상태를 예방하기 위한 방법을 설명한 것이다. 또한, 교착상태를 예방하는 경우는 제한된 리소스에 대한 접근을 베타적으로 허용하게 하는 것과 관련된 것이기 때문에 잘 알려진 몇 가지 패턴들이 소개되어 있다.

이러한 디자인 패턴 중에 잘 알려진 것이 잠금(lock)과 상호배제(Mutex)이다. 닷넷에서는 이들을 하나의 클래스 형태로 제공하고 있으며, 이에 대한 C++ 구현을 이전 연재 기사에서 소개했었다. 그러한 코드를 응용하여 여러분 고유의 잠금과 상호배제 클래스를 구현할 수 있을 것이다.

Mutex의 못다한 이야기

Mutex에 대해서 소개하면서 꽤 장황하게 설명한 느낌이 들었다. 하지만 그러면서도 제대로 설명하지 못했다는 생각을 굉장히 많이 했던 것도 사실이다. 다음 이야기는 앞의 기사에서 소개한 임계영역과 뮤텍스를 읽었거나 이미 잘 알고 있다는 전제 하에 설명하는 것이다.

Mutex는 커널 객체이기 때문에 프로세스 영역에 있는 임계영역과는 다르다. 다시 말해 임계 영역을 사용한 동기화 보다는 그 속도가 느리다. 그러나 커널 객체이기 때문에 응용 프로그램 프로세서 경계 건너편에 존재한다. 더 쉽게 말하면 두 응용 프로그램 프로세스가 동일한 뮤텍스를 소유할 수 있으며, 응용 프로그램 간에 동기화를 할 수 있는 방법을 제공한다. 더욱 재미있는 것은 각각의 응용 프로그램은 각자 다른 핸들을 가지지만, 실제로 커널 객체에 있는 동일한 뮤텍스를 가리킬 수 있다는 것이다.

즉, 두 응용 프로그램이 각각 뮤텍스에 대한 핸들 7, 10을 갖고 있고, 각자 다른 뮤텍스를 소유하고 생각할 수 있지만 실제로 두 핸들이 동일한 주소 0x8000 0000를 가리킬 수 있다는 것이다. 이들 핸들은 32비트 정수형이다. 앞의 기사에서 설명한 뮤텍스 예제에서는 뮤텍스에 대한 이름을 지정하지 않았기 때문에 각 응용 프로그램간에 독립적이었으며, 같은 뮤텍스를 소유하게 하려면 Named Mutex를 사용한다. 복잡한 것은 아니고 뮤텍스를 생성할 때 뮤텍스에 대한 이름을 지정하면 된다.

Process 클래스의 못다한 이야기

앞에 연재한 기사들 중에서 Process 클래스에 대해서 설명했고, 시스템에 있는 프로세스 정보를 출력하는 간단한 예제를 제시한 적이 있다. Process 클래스에 대한 자세한 내용은 MSDN을 참고하길 바라지만 자주 사용하는 메소드와 속성을 정리하면 다음과 같다.
  • Start, Stop
  • Handle
  • WairForExit
Start와 Stop을 사용해서 프로세스를 시작하고 종료할 수 있다. Win32 환경에서 프로세스는 쓰레드에 대한 컨테이너 역할을 하기 때문에 프로세스를 종료하면 그 프로세스에 속한 자식 쓰레드들도 모두 종료된다는 것을 기억하기 바란다. Process.Handle을 사용해서 현재 사용중인 프로세스의 핸들을 얻을 수 있다. 프로세스의 핸들을 얻음으로써 다른 응용 프로그램의 프로세스에서 정보를 얻을 수 있다. 게다가 프로세스를 조작할 수 있으며, Named Mutex를 복제하여 응용 프로그램의 Mutex로 지정해 줄 수도 있다. WaitForExit는 프로세스가 종료될 때 까지 기다리는 것을 지시하며, 이것은 주로 이벤트와 관련하여 처리한다. 간단히 말해 긴 시간 동안 파일을 다운 로드하거나 레코딩 작업, 디스크 정리 작업등을 끝낸 다음에 자동으로 응용 프로그램을 종료하거나 서버 프로세스를 종료하는데 사용할 수 있다.

쓰레드 풀링(Thread Pooling)

쓰레드 풀링에 대해서 자세히 설명할 기회를 갖지 못한 것을 아쉽게 생각한다. 쓰레드 풀링은 시스템의 자원을 효율적으로 사용하기 위한 대안이다. 시스템에서 쓰레드를 생성하고, 제거하는 작업 역시 많은 부하를 요구한다. IIS와 같은 웹 서버는 사용자가 웹 페이지를 요청할 때 쓰레드를 생성하고, 웹 페이지에 대한 요청을 완료하면 쓰레드를 종료한다. 사용자가 많은 사이트, 예를 들어 1초에 300명의 동시 사용자가 있다고 할 경우에 매 초마다 300개의 쓰레드를 생성하고 제거하는 것은 상당히 비효율적이다.

따라서 서버는 50개 정도의 쓰레드를 쓰레드 풀(Thread Pool)에 보유하고 사용자의 요청이 들어오면 쓰레드 풀에 있는 자유 쓰레드(Free Thread)가 쓰레드 풀 밖으로 나와서 요청을 처리하고, 나머지 작업에 대해서는 작업자 쓰레드(Worker Thread)에 일임하고 다시 쓰레드 풀로 돌아오는 방식을 취한다. 여기서 작업자 쓰레드는 실제 쓰레드가 아니기 때문에 의사 쓰레드 또는 가짜 쓰레드(Pseudo Thread)라고 얘기한다. 사용자가 의사 쓰레드에 작업을 요청하면 다시 쓰레드 풀에 있는 진짜 쓰레드가 나와서 요청을 처리하게 된다.

이러한 과정에서 알 수 있는 것처럼 쓰레드 풀링은 긴 처리시간을 갖는 쓰레드에 대해서는 그다지 적합하지 않으며, 생성과 제거 과정이 빈번한 멀티 쓰레드 프로그래밍에 적합하다. 이외에 쓰레드 풀링이 제공하는 장점과 단점을 정리해보면 다음과 같다.

쓰레드 풀링의 장점
  • 쓰레드 생성시 발생하는 문제 한정
    멀티 쓰레드 프로그래밍을 할 때, 여러 쓰레드들을 다루는 대신 쓰레드 풀로 일원화하여 다룰 수 있기 때문에 쓰레드와 관련된 문제를 쓰레드 풀로 한정하여 다룰 수 있는 장점을 제공한다.
  • 제한된 시스템 자원 사용
    실제로 쓰레드 풀링을 사용할 경우 CPU 점유율과 메모리 사용량을 낮출 수 있으며, 보다 적은 수의 쓰레드로 많은 작업을 처리할 수 있다.
  • 적은 CPU 시간 소모
  • 사용자 UI의 응답성 향상
쓰레드 풀링의 단점
  • 쓰레드 수 만큼의 리소스 사용
  • 쓰레드 수 증가에 따른 스케줄링
  • 쓰레드 수 증가만큼의 응용 프로그램 복잡성 증가
이러한 얘기들은 쓰레드 풀링에 국한된 것이 아니며, 쓰레드 프로그래밍 자체에 대한 것이다. 오히려, 쓰레드 풀링의 단점이라기 보다는 쓰레드 풀링을 사용할 수 없는 경우에 대해 생각해야 한다.

쓰레드 풀링을 사용할 수 없는 경우
  • 작업에 특성 우선 순위가 필요한 경우
  • 실행 시간이 오래 걸려서 다른 작업을 차단해야 하는 경우
  • STA 쓰레드를 사용하는 경우
  • 쓰레드 ID가 필요한 경우
쓰레드 풀링을 사용하는 경우 의사 쓰레드를 사용하기 때문에 작업에 특정 우선 순위를 부여하거나, 개별 쓰레드를 직접 제어하는 것이 불가능하다.

쓰레드 풀링 주요 메소드

쓰레드 풀링은 RegisterWaitForSingleObject(), QueueUserWorkItem(), UnsafeRegisterWairForSingleObject()를 함께 사용하며, 대개의 경우 ReaderWriterLock 클래스와 함께 사용한다. 간단한 쓰레드 풀링 예제는 다음과 같다.
class TestPoolApp 
{
   static void Main()
   {
      string str = "Hello from ThreadPool";
      ThreadPool.QueueUserWorkItem(new 
          WaitCallback(TestCallback),str);
      Thread.Sleep(2000);
   }
   static void TestCallback(object state)
   {
      Console.WriteLine(state);
   }
}
타이머

타이머는 여러분이 생각하는 것과 달리 시스템과 독립되어 있다. 일반적인 시스템에 장착되어 있는 타이머는 다음과 같이 나눌 수 있다.
  • 실시간 시계: 컴퓨터의 메인 보드에 내장되어 있으며, 수은 전지를 통해서 째깍째깍 돌아가는 진짜 시계를 뜻한다.
  • 8253 칩: 8253 칩에서 시스템 클럭을 1초에 120만번을 만들어 내며, 이 클럭은 16비트 레지스터에서 카운트 한다. 16비트 레지스터를 사용하기 때문에 2의 16승이 될 때 마다 펄스를 한 번 발생시키고 레지스터를 다시 초기화한다. 즉, 120만 / 2^16 = 18만 번의 펄스가 발생하게 된다. 1초에 발생하는 펄스가 18만 번이므로, 이를 나누게 되면 1/18만번 = 0.055 초마다 한 번씩 펄스가 발생하게 된다. 운영체제는 이와 같은 펄스를 사용한다. 즉, 55ms초에 한 번씩 윈도우 메시지를 체크하고 가져간다.
닷넷에서의 타이머

닷넷에서는 두 가지 타이머를 제공한다. 앞에서 얘기한 것처럼 윈도우 타이머는 System.Windows.Forms.Timer 클래스이며, 55ms의 한계를 갖지만, 일반적인 용도로는 무리가 없다. 그러나 윈도우 타이머는 시스템의 다른 쓰레드 작업에 의해서 영향을 받을 수 있기 때문에 멀티 쓰레드 프로그래밍에는 적합하지 않은 경우가 많다. 위에서 소개한 8253 타이머에서 발생하는 클럭은 CPU와는 무관하게 하드웨어에서 발생하는 것이다. 즉, 시스템이 바쁜지에 관계없이 항상 일정하게 클럭을 발생시킨다. 이와 같은 클럭을 사용하여 보다 정교한 제어가 필요한 곳에 사용할 수 있다. 이러한 클럭을 제공하는 타이머는 System.Timers.Timer 클래스에서 제공한다.

어떤 타이머를 사용하는 가는 독자의 선택이며, 경우에 따라 적절한 것을 사용하면 된다. VB6 프로그래머나 VC++과 같은 RAD 툴에서 제공하는 타이머는 닷넷의 System.Windows.Forms.Timer 클래스와 같은 것이며, System.Timers.Timer와 같은 정교한 타이머를 위해서는 VB6에서는 상용 컴포넌트를 사용하거나 VC++과 같은 다른 언어로 작성한 컴포넌트를 사용했다.

쓰레드 예외 처리 클래스 계층

쓰레드 예외 처리 클래스 계층 구조에 대해서 미흡하게 설명한 감이 있어서 여기에 그 클래스 계층 구조를 간단히 옮겨둔다. (참고로 MSDN에서는 참조하기가 너무 어렵게 되어 있다. -_-)
SystemException
  - ThreadAbortException
  - ThreadInterruptedException
  - ThreadStateException
  - SynchronizationLockException

MulticastDelegate
  - IOCompletionCallback
  - ThreadExceptionEventHandler
  - ThreadStart
  - TimerCallback, WaitCallback, WairOrTimerCallback

EventArgs
  - ThreadExceptionEventArgs
흥미있는 점은 IOCompletionCallback에 대한 것인데, IO 작업이 종료될 때 응용 프로그램을 종료할 수 있게 해준다. 이는 Win32 API인 WaitEx와 비교해보기 바란다.

ApartmentState

Thread 클래스에서 ApartmentState라는 것이 있다. VB6에서는 STA만을 지원했다는 것은 익히 알려진 사실이다. 굳이 여기서 VB6를 언급하는 것은 VB6로 작성한 컴포넌트를 닷넷에서 사용할 경우에는 이 ApartmentState를 특별히 지정할 필요가 있다. 이 속성은 COM과의 하위 호환성을 위해서 제공된다고 생각하면 된다.

STA와 MTA

ApartmentState는 STA와 MTA 구조가 있는데, Single Threaded Apartment라는 것은 하나의 쓰레드가 아파트를 소유하는 구조를 갖는다. 즉, 멀티 쓰레드를 제공할 수 없는 구조로 되어 있다. MTA는 Multi Threaded Apartment로 되어 있으며, 하나의 아파트에 여러 쓰레드가 세들어 살 수 있는 구조를 갖으며, 각각의 쓰레드가 독립된 방을 갖는 것이 허용된다. 즉, 멀티 쓰레드를 제공하기에 적합한 구조라고 할 수 있다. 이들을 지정하는 것은 다음과 같다.
    Thread.CurrentThread.ApartmentState = ApartmentState.MTA;
    Thread.CurrentThread.AprartmentState = ApartmentState.STA;
이와 같이 지정하는 방법도 있으며, 특성(attribute)을 사용하여 지정하는 방법도 있다. 이들은 System.STAThreadAttribute와 System.MTAThreadAttribute에 정의되어 있으며, 메소드 앞에 [STAThread] 또는 [MTAThread]와 같이 지정하면 된다. 주의할 점은 이미 시작된 쓰레드에 대해서는 아파트먼트를 지정할 수 없으므로 쓰레드를 시작하기 전(Thread.Start()를 호출하기 전)에 설정해야 한다.

닷넷에서는 MTAThread가 기본 설정이며, 이는 COM 객체와도 잘 맞는다. 다만, VB6로 작성한 DLL에 대해서만 호환성을 위해 STAThread를 사용하여 COM 객체에 대한 현재 쓰레드를 STA로 설정하도록 해야한다.

Reader/Writer Model

독자/작가 모델은 생산자/소비자(Producer/Consumer) 모델이라고도 한다. 응용 프로그램에서 쓰레드를 사용하여 데이터를 생산하고, 데이터를 소비한다면 데이터 생산과 소비가 하나로 연결되어 있기 때문에 효율적으로 처리할 수 없을 것이다. 이와 경우에 쓰레드 사용시 발생하는 부하까지 고려한다면 더 비효율적이다. 따라서 생산자와 소비자를 각각의 쓰레드로 분리하여, 생산자 쓰레드는 최대한의 데이터를 생산해내고, 소비자 쓰레드는 최대한 데이터를 소비하는 구조가 보다 효율적일 것이다. 닷넷에서는 이들을 ReaderWriterLock 클래스의 형태로 포장하여 제공하고 있으며, 주요 속성과 메소드는 다음과 같다.
속성
  - IsReaderLockHeld
  - IsWriterLockHeld
  - WriterSeqNum

메소드
  - AcquireReaderLock
  - AcquireWriterLock
  - ReleaseReaderLock
  - ReleaseWriterLock
각 이름에서 알 수 있는 것처럼 현재 독자(Reader)가 락을 소유하고 있는지, 작가의 일련 번호가 어떻게 되는지 알 수 있으며, 독자와 작가 모두 락을 획득하고 해제할 수 있는 메소드를 제공한다. ReaderWriterLock은 쓰레드 풀링과 함께 사용하여 하나의 동기화 객체에서 많은 수의 데이터를 처리할 때 유용하다.

생산자/소비자 모델은 경우에 따라 다음과 같이 4가지로 나눌 수 있다.
  • 단일 생산자/단일 소비자
  • 단일 생산자/다중 소비자
  • 다중 생산자/단일 소비자
  • 다중 생산자/다중 소비자
단일 생산자/단일 소비자

이러한 모델들은 서로간에 데이터를 공유하지 않는다. 이벤트에서 설명한 예제에서와 같이 한 쓰레드가 데이터를 읽어서 처리하고, 처리가 끝난 데이터를 다른 쓰레드에 넘겨주는 형태를 띄게 된다. 즉, 생산자와 소비자 간에 데이터가 있음과 없음을 알려주는 이벤트를 사용하는 형태가 된다. 이러한 단일 생산자/단일 소비자 모델은 pipelined 처리라고도 한다.

이러한 pipe-lined 처리의 대표적인 예로는 멀티미디어 플레이어가 있다. 멀티미디어 플레이어는 데이터를 읽어 들이고, 데이터를 디코딩하고, 데이터를 화면에 표시하는 것으로 나눠진다. 이들 쓰레드는 데이터를 공유하지 않으며 처리되는 데이터를 넘겨받는다. 즉, A → B → C 순으로 각 쓰레드가 처리한 데이터를 넘기는 형태를 띈다.

또는 소수를 찾는 알고리즘을 작성한다고 할 때, 루프를 돌면서 무작정 수행하는 것은 바람직하지 않다. 이런 경우에 소수를 찾아서 버퍼에 저장하는 쓰레드와 버퍼에 데이터가 있는지 검사하여 화면에 데이터를 뿌려주는 쓰레드를 같이 나누어서 처리하면 보다 효율적으로 처리할 수 있을 것이다.

특히 소수를 찾는다고 했을 때 2로 나누어지는지 확인하는 쓰레드, 3으로 나누어지는지 확인하는 쓰레드, 5로 나누어지는지 확인하는 쓰레드, 그리고 7과 같이 발견된 소수를 소수를 유지하는 버퍼에 집어넣고, 7로 나누어지는지 확인하는 쓰레드를 동적으로 생성하고, +2씩 더해나가면서 소수를 찾다가 11을 발견하게 되면 11로 나누어지는지 확인하는 쓰레드를 동적으로 생성하는 구조를 작성했다고 하자. (소수에서 짝수는 2 밖에 없으므로 짝수는 무시하고 +2씩 증가시키면 된다.) 이처럼 각각을 나누는 함수를 쓰레드로 나누고, 추가된 소수에 대해서 확인하는 쓰레드를 동적으로 생성하면 프로그램의 유연성을 높이고, 보다 빠르게 실행되는 쓰레드를 작성할 수 있다. 즉 11 까지 소수를 발견했을 때 진행되는 처리 순서는 다음과 같이 될 것이다. (편의상 2로 나누는 쓰레드는 2T와 같이 표현한다고 하자.)
  2T -> 3T -> 5T -> 7T -> 11T
        2T -> 3T -> 5T -> 7T -> 11T
              2T -> 3T -> 5T -> 7T -> 11T
이와 같은 구조에서 알 수 있는 것처럼 소수를 확인하는 전체의 알고리즘을 끝내기 전에 한 단계를 수행하고, 바로 다음 단계를 수행할 수 있다. 즉, 각 단계의 처리시간을 1이라고 산정할 경우에 선형적인 모델은 15라는 처리 시간이 걸리지만 위와 같은 pipe-lined 구조에서는 7이라는 시간밖에 걸리지 않는다.(흔히들 말하는 CPU의 파이프 구조라는 것이 이것을 뜻한다. 이 파이프 구조가 뭐부터 적용되기 시작했는지 기억은 안나지만, 아마 펜티엄 이상은 다 채택된 구조로 기억하고 있다.)

단일 생산자/다중 소비자

우리가 흔히 네트워크에서 접하는 온라인 게임들에 해당한다. 이러한 온라인 게임들은 하나의 서버가 가상의 세계에 대한 실시간 연산을 수행하며, 클라이언트는 이러한 서버의 데이터를 가져와서 읽어들이고 해석하여 적절한 그래픽으로 화면에 뿌려주는 것이다. 온라인 게임 외에 가장 흔히 보는 예로는 온라인 방송도 있다. 하나의 음악 서버에서 음악 데이터를 처리하고, 클라이언트는 이 방송 서버에 연결하여 데이터를 수신하여 처리하는 것이다. 서버에서는 하나의 데이터만 생성하지만 클라이언트는 1000개가 될 수 있다.

다중 생산자/단일 소비자

생산자가 여럿이고, 소비자가 하나인 모델에는 어떤 것이 있을까? 가장 일반적인 것은 큐를 생각할 수 있다. 네트워크에 연결된 프린터를 생각해 볼 경우 여러 PC들이 프린터로 출력 작업을 보낸다(다중 생산자). 반면 출력 작업은 하나의 프린터에서만 발생한다(단일 소비자). 이러한 구조를 다중 생산자/단일 소비자 모델이라고 한다. 이와 비슷해 보이지만 실제로는 다른 의미를 갖는 것들도 있다.

다중 생산자/다중 소비자

이것은 생산자와 소비자가 여럿인 경우로 대표적인 것은 항공사의 예약 시스템을 들 수 있다. 각 공항의 데스크와 사무실에서 좌석 예매를 처리할 수 있으며, 고객은 언제든지 온라인에서 남아있는 좌석과 좌석 수를 확인하고 예매를 할 수 있다. 혹시나 이러한 항공사 시스템에 대해서 궁금한 분들은 인터넷에 찾아보면 꽤 많은 자료들이 있고, 재미난 자료들도 있으니 찾아보기 바란다. 80년대에 IBM이 행했던 항공사 예매 시스템에 대한 자료도 있으며, 90년대에 공항 안내 화면에 MS의 블루 스크린이 떴다는 이야기까지 다양한 것들이 준비되어 있을 것이다. ^^;

비동기 대리자(Asynchronous Delegates)

동기 대리자에는 Invoke 메소드가 있으며, 비동기 대리자에는 IAsyncResult 인터페이스가 있다. 실제로 웹의 특성상 동기처리는 거의 사용하지 않으며, 웹 서비스와 관련하여 비동기 서비스를 사용하게 된다. 즉, 사용자가 지역과 지역을 지정하면 지역간 최단 이동경로를 표시하면서 두 지역간의 주요 위치 정보를 가져오는 각각의 쓰레드가 있다고 하면 처리 페이지에서 IAsyncResult를 사용하여 세 개의 쓰레드가 모두 종료될 때 까지 대기하면서 다른 처리를 계속하여 수행할 수 있고, 처리가 종료되었다고 알려올 때 마다 그에 따른 작업을 처리할 수 있을 것이다.

비동기 처리에 쓰이는 메소드는 BeginInvoke와 EndInvoke 메소드가 있다. 자세한 것은 MSDN을 찾아보기 바란다. 실제로 동기 처리를 많이 하지 않는다면 IAsyncResult와 대리자를 사용하여 쓰레드 사용을 캡슐화할 수 있기 때문에 쓰레드 자체를 거의 쓰지 않게 될 것이다.

IAsyncResult 인터페이스

System.Runtime.Remoting 네임 스페이스에 정의되어 있다. AsyncResult 콜백을 사용하며, IsCompleted 속성을 사용하여 작업의 완료 유무를 확인할 수 있다. AsyncResult 클래스는 비동기 작업을 캡슐화했으며, WebClientAsyncResult 클래스는 XML 웹 서비스 프록시에서 비동기 메소드 패턴을 구현하는데 사용하기 위해 IAsyncResult 인터페이스를 구현한 것이다. 비동기 처리에 대한 예제는 다음과 같다.
using System;
using System.Threading;
using System.Runtime.Remoting.Messaging;

delegate int Compute(string s);

public class Test
{
  static int TimeConsumingFunction (string s)
  {
    return s.Length;
  }

  static void DisplayFunctionResult(IAsyncResult ar)
  {
    Compute c = (Compute)((AsyncResult)ar).AsyncDelegate;
    int result = c.EndInvoke(ar);
    string s = (string)ar.AsyncState;
    Console.WriteLine("{0} is {1} characters long", s, result.ToString());
  }

  static void Main()
  {
    Compute c = new Compute(TimeConsumingFunction);
    AsyncCallback ac = new AsyncCallback(DisplayFunctionResult);
    string str1 = ".NET Framework";
    string str2 = "Seren";

    IAsyncResult ar1 = c.BeginInvoke(str1, ac, str1);
    IAsyncResult ar2 = c.BeginInvoke(str2, ac, str2);
    Console.WriteLine("Ready");
    Console.Read();
  }
}
비동기 웹 서비스 구현

비동기 웹 서비스를 구현하는 경우에도 IAsyncResult 인터페이스를 구현한 것을 사용하며, WebClientAsyncResult 클래스를 이용한다. 아래 코드는 간단히 구현한 비동기 웹 서비스의 일부를 옮긴 것이다.
arStart = mapService.BeginGetMap(start, null, null);
arFinish = mapService.BeginGetMap(finish, null, null);
arDirections = mapService.BeginGetDirections(start, finish, null, null);

WaitHandle [] wh = { 
     arStart.AsyncWaitHandle, 
     arFinish.AsyncWaitHandle, 
     arDirections.AsyncWaitHandle 
};
    
WaitHandle.WaitAll(wh); 
((WebClientAsyncResult)arDirections).Abort();
교착상태 검출(Deadlock Detection)
Never know I"m here.(내가 여기있는걸 절대 모를걸) - Ghost in StarCraft
앞에서 교착상태가 발생하는 네 가지 경우에 대해서 이야기 했고, 그 중 두 가지는 운영 체제가 책임지는 부분이며, 다른 두 가지는 프로그래머가 책임져야 한다고 했다. 또한 프로그래머는 교착상태 검출과 예방에 힘쓰며, 대개의 경우에 예방에 힘쓴다고 했다. 실제로 교착상태를 검출할 수 있는 기능을 닷넷 프레임워크는 제공하지 않는다. (사실, 거의 대부분의 언어가 제공하지 않는다.)

교착상태를 검출하려면 다중 프로세서를 사용하는 PC가 있어야 한다. 그리고 리눅스 서버나 듀얼 메인보드라고 하여 시중에서 판매되는 메인 보드에 일반 펜티엄 III CPU를 2개씩 꽂는 것으로는 제대로 테스트할 수 없다. 반드시 ServerWorks등을 사용한 서버용 메인보드와 Xeon과 같이 병렬처리를 위해 설계된 CPU를 2개 이상 설치한 시스템이 있어야 한다. 만약, 이와 같은 환경이 갖추어지지 않았다면 자신이 작성한 코드가 싱글 프로세서 머신에서 잘 수행된다고 하더라도, 다중 프로세서 머신에서는 제대로 수행되지 않는다고 생각하기 바란다.

다중 프로세서 머신에서 테스트할 수 있는 좋은 방법은 Solaris와 같은 UNIX 시스템의 계정을 얻어서 프로그래밍을 하는 것이다. 대개의 서버 머신들은 병렬 처리를 위해 설계되어 있으며, 2개 이상의 CPU가 설치되어 있다.(보통 2개 내지 4개 정도가 장착되어 있으며, 장착된 CPU의 수는 dmesg를 사용해서 알 수 있다. 윈도우 NT/2000/XP 환경이라면 set을 입력하면 PROCESSOR_*로 시작하는 환경 변수에서 CPU 정보를 얻을 수 있다.)

이제, 검출에 대해서 이야기 해보자. 교착상태 검출이라는 것은 특별한 것은 아니다. 메모리에 로드되어 특정 함수가 호출되는가를 감시하는 것이다. 그리고 이러한 함수들을 호출하는 것을 발견하면 해당 쓰레드 ID, 쓰레드의 핸들(32비트 값이며, 보통 어드레스), 입력값, 반환값, 호출/반환등을 간단히 로그파일에 기록하는 것이다. 응용 프로그램을 테스트하다가 교착상태에 빠지게 되면 응용 프로그램을 종료하고, 로그 파일을 확인해서 어느 위치에서 교착상태가 발생하는지 쉽게 알 수 있다. 로그 파일에 기록된 어드레스를 토대로, 컴파일시 생성한 디버깅 정보를 이용해서 디버거에서 문제를 동일하게 재현하고, 문제를 해결할 수 있다.

그렇다. 내용을 알고나면 별거 아니다. 그러면 뭐가 문제인가? 실제로는 교착상태 검출을 위해서는 검출 루틴 자체가 메모리에 로드되어야 한다. 또한, 실행 파일 뿐만 아니라 실행 파일이 사용하는 DLL의 복사본이 메모리에 로드된다는 것은 익히 알려진 사실이다. 이처럼 메모리상에 로드된 DLL에서 특정 함수가 호출될 때를 가로챌 수 있어야 한다. 이와 같이 특정 메시지를 가로채는 것을 "갈고리로 채 간다"라는 개념으로 "후킹(hooking)"이라는 용어를 사용한다.

닷넷에서는 후킹에 대한 메소드를 제공하지 않는다(베타1에는 있었지만, 이후에 제거되었다). 따라서 Win32 API 함수를 DllImport 특성을 사용해서 선언하여 사용하도록 한다. (실제로 DLL에서 호출되는 함수에 대한 어드레스 매핑등에 대한 정보는 .idata 섹션에 저장된다. 이러한 실제 구현에 대해서 궁금하다면 PE File Format에 대한 문서를 읽어보고, COFF(Common Object File Format)을 참고하면 도움이 될 것이다. 그리고 마이크로소프트웨어 2002년도 중에 PE File Format에 대해서 자세히 설명한 기사가 있었던 것으로 기억한다.) 닷넷에서 후킹을 위해 사용할 Win32 API는 대부분의 경우에 SetWindowsHookEx, UnhookWindowsHookEx, CallNextHookEx, CopyMemory, MoveMemory 정도가 될 것이다(닷넷에서는 윈도우 메시지를 가로채서 처리할 수 있는 IFilterMessage와 윈도우 폼에서 컨트롤에 대해 사용할 수 있는 HookChildControls 외에는 제공하지 않고 있다).

후킹을 하려면 WH_*와 같은 윈도우 핸들를 정의하도록 한다. CallNextHookEx에서 후킹할 것들을 지정할 수 있으므로 원하는 윈도우 핸들을 지정해서 처리할 수 있을 것이다. 셸에 대해서 처리하고 싶다면 WH_SHELL을, 메시지 처리를 닷넷의 IFilterMessage 대신에 직접 처리하고 싶다면 WH_MSGFILTER를, 시스템 메시지 처리에 대해서는 WH_SYSMSGFILTER를, 키보드를 처리하고 싶다면 WH_KEYBOARD를 사용하며, Alt+Tab, Ctrl+Esc와 같은 특수 키를 처리하고 싶다면 저수준의 키보드 핸들인 WH_KEYBOARD_LL을, 컴퓨터 기반 트레이닝에 대한 처리는 WH_CBT를, 디버그에 대해서는 WH_DEBUG, 마우스에 대해서는 WH_MOUSE, WH_MOUSE_LL를 각각 사용할 수 있다. 이러한 후킹을 처리하는데 필요한 구조체를 C#에서 정의하려면 구조체를 정의하는 struct 키워드 앞에 StructLayout 특성(attribute)을 함께 사용한다.
[StructLayout(LayoutKind.Sequential)]
public struct MOUSEHOOKSTRUCT
{
  public POINTAPI pt;
  public int hwnd;
  public int wHitTestCode;
  public int dwExtraInfo;
}
StructLayout 특성에 대한 자세한 설명은 MSDN을 참고한다.
Concurrent Abstraction Programming
여러 가지가 있겠지만 여기서는 현재 C#에서 제한적으로 구현되어 있는 것에 대해서 소개하려한다. CAP라는 것은 쓰레드 프로그래밍과 같은 것들을 말 그대로 추상화시켜 일반화한다는 얘기다.

비동기 메소드
async postEvent(EventInfo data)
{
  // large method body
}
비동기 메소드는 async 키워드로 구현한다.

Chords

동기 메소드와 비동기 메소드를 조합한 것을 Chords라 한다.
class Buffer
{
  string get() & async put(string s)
  {
    return s;
  }
}
위 코드는 버퍼를 구현한 것이며, 이 버퍼를 이용하는 코드는 다음과 같다.
Buffer buff = new Buffer();
buff.put("blue");
buff.put("sky");
Console.Write(buff.get() + buff.get());
먼저 메소드 선언의 왼쪽은 동기 메소드이고, 오른쪽은 비동기 메소드다. 먼저 buff.put("blue")를 사용해서 버퍼에 데이터를 집어넣는다. put 메소드는 비동기 메소드이므로 처리가 완료된다. 두 번째 buff.put("sky")도 마찬가지로 버퍼에 데이터를 집어넣으며 비동기(async)이므로 문제없이 처리가 완료된다.
Console.Write(buff.get() + buff.get());
여기서 get()을 호출한다. chords로 정의된 메소드는 두 조건이 일치할 때만 수행된다. 즉, 먼저 비동기 메소드 put을 호출하여 비동기 메소드 조건을 만족시키고, get을 호출하여 동기 메소드 조건을 만족시킬 때 블록 안의 문장 return s;가 실행된다. 결과는 버퍼에 들어가 있던 "blue"를 꺼내오고, 두 번째는 "sky"를 꺼내온다. 따라서 싱글 프로세서 머신에서는 항상 "bluesky"라는 결과를 보게되며, 다중 프로세서 머신에서는 경우에 따라 "bluesky" 또는 "skyblue"라는 결과를 보게된다.
class Buffer {
  string get() & async put(string s) {
    return s;
  }

  string get() & async put(int n) {
    return n.ToString();
  }
}
이것은 버퍼 클래스에 대해서 오버로딩을 적용한 것이다. 이제 버퍼에는 string 형뿐만 아니라 int 형도 저장할 수 있게 된다.
class OneCell()
  public OneCell() { empty(); }

  public void put(Object o) & async empty() {
    contains(o);
  }
  public Object get() & async contains(Object o) {
  empty();
  return o;
  }
}
이것은 셀을 사용하여 데이터를 저장하는 형태를 가진다. OneCell 클래스를 초기화할 때 생성자에 있는 empty()라는 메시지가 전달된다. 두 번째 메소드에서 async empty()이므로 문제 없이 수행되고, 조건을 만족한다. 이제 put을 사용해서 어떤 객체를 셀에 저장할 경우 put과 empty의 조건을 만족하므로 블록 안에 있는 contains(o);라는 메시지가 전달된다. 이 메시지는 async contains()에서 받아서 처리하게 된다. get이 호출되면 이제 세 번째 메소드도 조건을 양쪽 모두 만족하게 되기 때문에 empty() 메시지를 전달하고, 객체 o를 반환한다. 이러한 셀 구조를 사용해서 어떤 형태의 데이터든 저장하고, 가져올 수 있다.

객체 지향 프로그래밍이라고 하면 "현실 세계를 모델링한다."라는 표현을 사용한다. 그리고 객체와 객체 사이에 메시지를 주고 받는다고 하지만 객체 지향 언어에는 그 메시지 자체가 숨겨져 있다. 그러나 위에서 볼 수 있는 것처럼 Concurrent Abstraction Pattern에서는 이러한 메시지를 공개하고, 프로그래머가 직접 정의하고 사용할 수 있게 해준다. 이것이 쓰레드와 무슨 상관이 있는 걸까? 조금만 더 보자.
class ReaderWriter {
  ReaderWriter() { Idle(); }
  public void Shared() & async Idle() { S(1); }
  public void ReleaseShared() & async S(int n) {
    if ( n == 1 ) Idle(); else S(n-1);
  }
  public void Exclusive() & async Idle() {}
  public void ReleaseExclusive() { Idle(); }
}
이것은 ReaderWriter를 클래스로 구현한 것이다. 다른 부분은 모두 비슷하니 무시하고, public void ReleaseShared() & async S(int n) 부분만 설명하겠다. 이 부분은 n이 1이 될 때까지 계속해서 펌프질을 해서 공유된 데이터를 비우라는 것을 뜻한다.
class Token {
  public void Grab() & public async Free() {}
  public Token(int n) { for(; n-- > 0;) Free(); }
}

class Heavy {
  static Token tk = new Token(2); // limit parallelism

  public Heavy (int q) { tk.Grab(); …; } // rather slow
  public int Work(int p) { return …; } // rather fast
  public void Close() { tk.Free(); }
}
이것은 사용자 지정 스케줄러(Custom Scheduler)를 구현한 것이다. 단 몇 줄로 쓰레드에 대한 스케줄러를 작성할 수 있다는 것을 쉽게 알 수 있을 것이다. 게다가 이러한 메소드 조인은 두 개의 메소드만 조인할 수 있는 것이 아니라 3개 이상의 메소드를 조인할 수도 있다. 이러한 메소드를 간단히 Join 메소드라고 부르며, C# 컴파일러는 이것을 해석하여 일반 클래스로 재작성한다(컴파일 과정일 뿐이다). 이것은 아직까지 소개되지 않았고, 언제쯤 Join Method가 적용될지는 아무도 모른다. 그러나 Concurrent에 대한 문제를 해결하기 위한 노력은 계속 진행되고 있다.

알려지지 않은 이야기
The truth is out there.(진실은 저 너머에 있다) - Chris Carter
동시성(Concurrent) 제어를 컴파일러를 통해 어셈블리(기계어) 수준에서 제어할 수 있다는 것을 70년대에 증명하고, 컴파일러로 구현한 것이 Concurrent Pascal이다. 1975년에 등장한 이 Concurrent Pascal은 플랫폼에 독립적이이면서, 병렬 처리 프로그램을 모니터를 사용해서 안전하게 프로그래밍할 수 있게 해준다.

현대 프로그래밍은 대부분 네트워크를 사용하고, 미들 서버 프로그래밍의 경우에는 쓰레드를 사용하여 보다 효율적으로 처리할 수 있다. 그러나 많은 언어들이 Concurrent Pascal의 아이디어를 채용하고 있는 것은 아니다. 이중에 쓰레드와 동기화를 제공하는 언어에는 자바와 닷넷 플랫폼이 있다.(닷넷 플랫폼 기반의 언어는 모두 동일하므로 C#이 아닌 닷넷 플랫폼이라 지칭한다)

그러나 자바에서는 모니터와 같은 핵심적인 구현을 생략했기 때문에 Concurrent 분야에 대한 지난 25년간의 연구를 모두 무시하는 실수를 저질렀다고 비난받고 있다. 그에 비해 닷넷 플랫폼은 모니터와 같은 도구들을 제공하고 있지만, 쓰레드에 대한 여러 가지 클래스와 메소드들의 의미가 상당히 모호하다는 평가를 받고 있다.

자바는 스펙에서 쓰레드에 대한 예외 처리 부족과 매우 빈약한 지원으로 공격당하고 있다. 마찬가지로 닷넷의 쓰레드는 모두 Win32 API 함수에 대한 단순한 래퍼(wrapper)로 되어 있으며, Win32 API 쓰레드의 고질적인 문제점 "당신이 생각한 데로 정확하게 동작하지 않는다"라는 문제점을 지적받고 있다. 사실, Win32 API 전체가 공격받고 있는 것은 아니며, WaitForMultipleObjects()에 대해서 공격받고 있다. 사실상 POSIX Thread(이하 PThread)에서는 WaitForMultipleObjects를 사용하지 않고도 문제를 유연하게 처리할 수 있다. 실제로 이러한 부분들은 자바와 Win32 진영 모두 쉬쉬하고 있는 숨겨진 진실이다.

독자들은 이제 슬슬 궁금해질 것이다. "trax씨, 당신은 지금까지 닷넷 쓰레드에 대해서 설명해오지 않았나요? 그런데 지금와서 이것을 모두 부정하면 어쩌라는 거지요?"

부정하는 것은 아니다. 여러분은 어떤 언어를 사용하든지 충분히 훌륭하게 멀티 쓰레드 프로그래밍을 할 수 있다. 자바와 닷넷 플랫폼이 나름대로의 장단점을 갖고 있으며, 단점이 있으면 그것을 피해나갈 수 있을 것이다. 사실, 닷넷 플랫폼은 Win32 쓰레드의 문제점을 해결하고, 의미를 명확하게 하려 했지만, 반은 성공, 반은 실패했다고 생각한다. 문제를 명확하게 나눈 부분도 있지만, Win32 API에 대한 생각이 지배적인지 Win32 API의 사상을 그대로 물려받은 부분들도 있기 때문에 필자는 그렇게 생각한다.

사실, 자바의 부족한 쓰레드 지원과 동기화 모델의 부족을 해결하기 위해서 ZThread(http://zthread.soureforge.net)가 발전중에 있다. ZThread는 POSIX를 준수하며, 객체 지향, 크로스 플랫폼을 위한 C++ 스레딩 패키지였다. 현재는 자바 버전도 함께 제공되기 시작하고 있으며, 사실상 자바에서 POSIX Thread를 사용하려는 사람들에게 많은 지지를 받고 있다.

Win32 환경에서 pthread를 사용하려면 PThread(http://sources.redhat.com/pthreads-win32/)를 사용할 수 있다. 이것은 Unix/Linux 환경의 pthread를 Win32 환경에 구현한 것으로 객체 지향, 크로스 플랫폼을 위한 스레딩 패키지이며, C/C++에서 사용할 수 있다. 마찬가지로, DllImport를 사용해서 닷넷 플랫폼에서 사용할 수 있지만, 관리되지 않는 코드(unamanged code)를 사용하는 것에 대해서는 의견이 분분하다. 자바와 마찬가지로 닷넷 플랫폼에 사용할 수 있는 POSIX Thread 패키지가 나온다면 보다 좋을 것이다.

특정 언어가 제공하는 스레딩 지원을 떠나 다양한 플랫폼에 적용할 수 있는 POSIX Thread를 접해보는 것도 중요하며, 자신이 사용하는 언어에서 제공하는 스레딩 모델의 장단점을 객관적으로 바라보고, 그것을 이해하여 개선해나갈 수 있는 방법을 찾아낸다면 더 이상 바랄 것이 없을 것이다.

쓰레드에 대한 많은 구루(guru)들은 자바의 스펙과 스레딩 모델의 지원은 빈약하기 그지없고, 불완전하기 때문에 데이터 훼손(data corruption)을 제대로 막을 수 없다고 비난하고 있고, 닷넷은 다양한 모델을 제공하지만, 생각하는 데로(코딩하는 데로) 동작하지 않는 경우가 많다고 비난하고 있다. 또한, 닷넷 플랫폼의 동기화 예제들 중에 몇몇은 교착상태가 발생하는 것을 예제라고 올려두었다며 심하게 비난하고 있다. (실제로 이러한 코딩은 교착상태가 발견합니다. 라는 예제로 MSDN 사이트의 예제를 인용하고 있으며, 조목조목 잘 설명해 놓았다.) 그리고 구루들은 한결같이 이러한 문제의 대안으로 PThread를 사용할 것을 권하고 있다.

결국, 쓰레드라는 미지의 세계는 어느 한쪽만 알아서도 안되고, 책을 너무 의지해서도 안된다. 많은 현인들의 지혜를 얻어오고 경험을 더 쌓아야 한다는 것을 알 수 있다. 닷넷 플랫폼에서의 쓰레드에 대해서 설명했지만, 구현 예제보다는 운영체제와 관련된 개념을 더 많이 전달하고 싶었는데, 제대로 되지 않아서 아쉽다. 관심이 많은 분들은 운영체제와 POSIX Thread, Win32 Threading Model, Memory Model에 대해서 학습하기 바란다.

지금까지 쓰레드에 대한 기사를 쓰면서 설명했던 예제나 미처 설명하지 못했던 예제들을 제공하려고 한다. 여기에는 필자가 원격으로 동적으로 IP를 할당받은 머신을 찾기 위해 작성한 멀티 쓰레드 포트 스캐너 같은 간단한 유틸리티도 있으며, 어셈블리(DLL)로 스레딩 코드를 작성하고, ASP.NET에서 멀티 쓰레드 검색엔진으로 사용하는 것을 설명한 것도 있다.

모쪼록 부족한 부분이나마 이 글이 닷넷을 포함한 쓰레드 전반에 대한 이해에 도움이 되었으면 싶다. (후일에 좋은 책으로 다시 만날 수 있기를 바란다.) 늘 함께해주는 주위 분들에게 고마움을 전하고, 쓰레드에 대한 어리석은 질문에 친절하게 설명해준 Alexsander Terekhov와 comp.programming.threads 그룹의 많은 구루(guru)들에게 감사드린다.

참고자료
  • Win32 멀티 쓰레드 프로그래밍, 한빛미디어
    Win32 멀티 쓰레드 프로그래밍에 대한 깊은 지식을 전달한다. 상당히 많은 코드와 세련된 프로그래밍 방법을 보여주기 때문에 쓰레드와 관련된 지식 뿐만 아니라 코딩에 대한 여러 가지를 배울 수 있다. 실제로 쓰레드 프로그래밍을 하면서 이 책에 수록된 예제들을 많이 참고하게 될 것이다. 이 책은 주로 실전적인 것들을 설명하고 있으며, Win32 쓰레드에 대한 필독서다.

  • Pthreads Programming, O"Reilly
    POSIX Threads에 대해서 다루고 있으며, 초보자에게 가장 적합한 입문서이다. 실제로 초보자를 위한 이 분야의 책은 이 책 뿐이다. 단점은 개념 설명 위주로 되어 있기 때문에 이 책을 보면서 실제 코딩에 적용하기는 어렵다. PThreads가 무엇인지 전혀 모른다면 꼭 봐야 할 책이다.

  • Programming Applications for Microsoft Windows, MS Press
    제목과는 달리 쓰레드 프로그래밍, 메모리 모델, 예외 처리에 대해서 설명하고 있는 책으로, 저자의 뛰어난 지식을 엿볼 수 있다. 윈도우 프로그래밍의 깊은 곳을 탐험하고 싶다면 이 책은 필독서로 꼽아야 한다. 사실, 이 책은 읽어보지 않았지만 이 책의 이전판인 『Advanced Windows NT』는 읽어보았다. 『Advanced Windows NT』의 개정판으로 이전판보다 스레딩에 대해 더 자세하게 설명하고 있다. (당연하지만 『Advanced Windows NT』는 절판되었다) 또한 윈도우 2000에 대한 내용을 포함하고 있다.

  • Modern Concurrency Abstractions for C#, Nick Benton, Luca Cardelli, Cedric Fournet, MS Research
    C#에서의 Concurrency와 비동기 프로그래밍에 대해서 설명한 논문이다.

  • Introduction to Polyphonic C#, MS Research
    비동기 프로그래밍에 대해 설명하고 있는 설명서다.

  • Performance Limitations of the Java Core Libraries, Allan Heydon, Marc Najork, Compaq Systems Research Center
    자바의 성능, 클래스 라이브러리, 스레딩에 대해서 설명하고 있다.

  • A Portable Object-Oriented C++ Thread Library
    객체 지향, 크로스 플랫폼을 지원하는 스레딩 라이브러리이며, 자바에 대한 패키지도 제공한다. 이곳에서 문서, 프로젝트 소스 코드를 얻을 수 있다. Win32, 자바, UNIX/Linux 환경을 모두 지원한다.

  • Programming POSIX Threads
    POSIX Threads 프로그래밍에 대한 방대한 정보와 링크를 제공한다. 이곳에서 여러분이 필요한 대부분의 것들을 얻을 수 있을 것이다.

  • POSIX Threads(pthreads) for Win32
    Pthreads에 대한 Win32 구현을 제공한다. Win32 환경에서는 이 라이브러리를 사용하여 pthreads 프로그래밍을 할 수 있다.

  • Next Generation POSIX Threading
    Linux에서의 POSIX Threads에 대한 프로젝트로 AIX, SGI와 같은 상용 OS에서 제공하는 수준의 스레딩과 SMP 머신에서 쓰레드를 제공하는 것을 목적으로 하는 프로젝트다.

  • Interprocess Communications
    프로세스간 통신에 대한 내용으로 MSDN이나 MSDN Online의 목차에서 검색하기 바란다. 순서는 Windows Development | Windows Base Services | Interprocess Communications이다. Windows Base Services | Memory는 Memory Model에 대한 깊은 설명을 제공할 것이고, 여기서 가상 메모리 관리, 힙, 쓰레드 로컬 스토리지(TLS)에 대한 구조와 자세한 설명 뿐만 아니라 특정 메모리 페이지 보호와 같은 깊은 내용들을 볼 수 있을 것이다. 특히, Putting DLDETECT to Work는 교착상태(DeadLock) 검출에 대한 자세한 설명과 실제 구현을 모두 보여주고 있다.
C# 쓰레드 이야기 소스 전체 다운로드: thread_source.zip
TAG :
댓글 입력
자료실

최근 본 책0