Professional Documents
Culture Documents
1
스레드, 그리고 Java 와 C#
중간중간에 코드 조각들을 갖고 얘기를 하지만, 실행되는 완전한 코드는 아니며, 완전한 실행을 위해서는
좋다는 얘기를 하려는 것은 아닙니다. 스레드에 대해서 얘기하려고 합니다. C# 이전에 스레드를 언어에서
지원하는 것은 Java 뿐이었죠. 프로그래밍 언어는 크게 명령형 언어와 함수형 언어로 나눌 수 있습니다. C
언어와 같은 구조적 프로그래밍, 또는 절차적 프로그래밍을 지원하는 언어들은 함수형 언어입니다. APL과 같은
언어는 진정한 함수형 언어로 볼 수 있습니다. 예… 그만큼 APL로 작성된 코드는 판독하기가 어렵습니다.
프로그래밍 언어에 있어서 유명한 저자인 코넬도 단 4 줄의 APL 코드를 분석하는 데 4 시간이 걸렸다고
C#과 같은 언어가 명령형 언어이고, C++보다는 Java나 C#과 같은 언어가 명령형 언어에 더 가깝습니다. 제가
말하는 명령형 언어와 함수형 언어는 일반적인 프로그래밍 언어의 구분과는 조금 다릅니다. 그러니까 LISP 같은
언어는 논의에 두지 않고 있는 거죠. 어쨌든 논의에서 제외한 LISP과 같은 언어를 빼면 명령형 언어나 함수형
저장하고, CPU에서 데이터와 코드를 조금씩 가져와서 차례대로 처리하는 것을 말합니다. 한 번에 조금만
메모리에 데이터와 코드를 배정하고, CPU에서 연산을 처리하고, 이 같은 일을 반복하는 루프를 갖는 구조를
대해서 한 번은 공부해두면 조금은 시야가 넓어진다고 생각합니다. 다시 논의로 돌아와서 대부분의 프로그래밍
그러면 왜 전통적인 폰 노이만 구조를 따르는 언어들 중에 Java와 C#과 같이 프로세스 제어, 즉 스레드를
지원하게 되었는지 궁금하지 않습니까? Java라는 언어가 나타나기 이전의 컴퓨팅 환경은 단일 태스크 작업
말하면 Java는 UI와 실제 처리를 위해서 스레드를 필요로 하기 때문이지만…. 어쨌든 그 이후로는 응용
2
멀티 태스크 환경을 이용한 스레드 제어의 필요성이 생겼다는 거죠. 이 때문에 윈도우 개발자들 사이에서
스레드는 최고의 유행어가 되었던 때도 있었죠. 하지만 언어 자체에서 스레드를 지원하는 것이 아니기 때문에
OS에서 제공하는 기능을 빌려 쓰거나, 직접 스레드를 제어하는 비표준 API를 만들어 써야 했습니다. 만약에
다른 OS로 옮긴다면 스레드와 관련된 부분은 다시 작성될 필요가 있었던 거죠. 그래서 이런저런 이유로 새로운
언어는 프로세스 제어 기능이 언어 설계에서 요구되고, 새롭게 설계되는 언어들은 프로세스 지향적인 성격을
띄게 되었습니다. 이러한 프로세스 지향적인 언어로는 Java, C#, Ada95, Perl이 있습니다. 멀티 태스크
환경에서 사용자는 더 많은 것들을 하기를 원하지만, 그것이 어떻게 돌아가는지는 신경 쓰지 않죠. 반대로
개발자는 그것이 어떻게 돌아가는지 신경 써야 하구요. -_-. 이러한 배경으로 인해서 스레드 프로그래밍을
일반적으로 하게되고, OS에서 빌려 쓰거나, Java와 같은 언어를 사용해서 스레드를 구현합니다. 스레드를
수 있고, 문법 검사를 수행하고 파일을 저장할 수 있습니다. 새로운 브라우저 창을 만들어도 새로운 프로세스가
시작하는 것이 아니라 하나의 스레드만 추가되기 때문에 공통되는 자원을 공유할 수 있고, 적은 메모리로 여러
독점하지 않는 프로그램을 작성할 수 있도록 해주며, 보다 고급 기술을 익힘으로써 프로그래머 자신의 가치를
나아가고 있다는 것이죠. 4baf : 얘기는 잘 들었습니다. 예, 자바는 스레드를 언어 차원에서 지원하는 첫번째
언어죠. 그리고 C#도 마찬가지로 스레드를 언어 차원에서 지원합니다. 하지만 자바보다 C#의 스레드 지원이
C#의 그것과 같은지 정확하게 말은 못하겠네요. 아무튼, 파이썬도 자바처럼 프로그래밍이 됩니다.
class PyClass(Thread) :
def __init__(self) :
Thread.__init__(self)
def run(self) :
작업
3
대강 이런 식이었던 것 같습니다. 동기화 부분은 공부를 안해서 모르겠네요. 사실 자료도 구할 수 없었습니다.
자바에서 스레드를 대부분의 프로그래머가 이용하지 않는다고 하셨는데, 아마 자바 스윙을 사용하는 사람들은
일찌감치 GUI 객체들을 생성해 놓지 않으면 다운된건지 아닌지 알 수가 없습니다. JSP/서블릿이나 EJB 도,
자동적으로 스레드에 의해서 실행 되죠. 초보자들은 그래서 자원에 대한 동기화를 안하고 결과적으로 진짜 CPU
두 개 달린 시스템으로 옮기면 완전히 죽어버리는 코드를 생산하게 됩니다. Trax : 제가 얘기하고자 하는 논지를
완전히 벗어나셨군요. -_-. 미들티어에서 COM/DCOM, MTS 와 통합된 COM+ 모두 스레드를 생각하지 않고는
특성에 기인합니다. (ColdFusion, JRun Server, ASP, JSP, PHP, 탱고) 제가 하고자 하는 말은 이러한 것을
얘기하는 것이 아니고 Java 와 C#을 비교하자는 것도 아닙니다. 언어의 계보를 따져보면 CPL -> BCPL -> B -
> C -> C++ -> Java -> C# 이 될 겁니다. 또는 C++의 다른 갈래로 C#이 될 것입니다. 물론 제가 얘기하고
라이브러리를 만들어 왔습니다. 그리고 일부는 이것을 라이브러리로서 판매해왔습니다. (Sheridan 의 Active
Thread 같은 라이브러리가 잘 알려져 있습니다.) 그러나 이것은 너무나 반복적인 작업이기 때문에 이제는 언어
설계 자체에 프로세스처리가 반영되고 있다는 얘기입니다. 물론, 가상 머신을 갖고 있으니 스레드 지원이 더
것입니다. 나중에는 프로그래머 지망생들은 당연히 스레드에 대해서 배우지 않을까요. 사실 Linux 나 Unix 에서
양치질 하면서 신문을 보는 엽기적인 행각을 벌이는 멀티 태스킹을 제대로 이해하지 못하고 상상도 하지
못합니다. 그리고 실제로 접하면서 이것이 도스와 다른 점을 깨닫고 멀티 태스킹에 대해 이해하려고 하지요. 즉,
4
멀티 태스크에 대해 장황한 말이 필요합니다. 하지만 처음부터 윈도우와 같은 멀티 태스크 OS 를 사용한
세대들에게는 당연한 것이죠. 마찬가지로 스레드와 같은 기술이 VC++에서도 있었지만, 사용의 어려움 때문에
그 수준조차 초등학교 수준이랄까... 수박 겉핥기를 벗어나지 못합니다. 그러나 앞으로의 언어들은 프로세스
지향적인 부분을 갖게 되고, 프로세스와 스레드는 기본이 될 것이고, 개발자가 이러한 것들을 처리하기위해
비슷비슷한 것들을 반복해서 개발하는 일에서 탈피할 수 있다는 것입니다. 예... 이것은 전적으로 제 개인적인
생각입니다. VB.NET 도 프로세스와 스레드에 대한 처리를 지원합니다. (CLR 때문이라 생각하고 별 것 아니라고
Perl 6 을 릴리스하기 위한 단계에 있는 Perl 조차 스레드를 지원하고 있습니다. 최근에 개발된 언어들은
얘기하는 것은... 네트워크, 스레드를 빼고는 앞으로 좋은 프로그래머가 될 수 없다는 얘기이고, 먼 시일을
주제입니다. 특별히 동기화를 주제로 빼고 싶은 생각은 없습니다. 어떤 언어를 쓰거나 상관하지 않습니다.
스레드라고 말하는가? 뭐... 스레드라면 단순하지만... C#을 보면 프로세스 자체를 제어할 수 있는 모니터,
뮤텍스를 클래스 차원에서 지원하고 있습니다(너무 단순하고 쉬워서 황당할 정도지요. 물론 기존 프로세스와
뮤텍스까지 자세히 알라는 얘기입니다. 지금까지도 프로그래밍 언어는 3~4 년에 하나씩 등장하고 있습니다.
예... 미안하게도 스크립트형 언어는 프로그래밍 언어라는 관점에서는 주목을 받지 않습니다. 왜냐면 그것은
기계어로 번역할 뿐입니다. 제가 얘기하는 것은 Java 와 C#이 주는 의미는 앞으로의 경향을 단적으로 보여주는
자식 스레드들간의 정보 교환이나 관리하는 역할을 합니다. 예로, 어머니가 자신의 귀여운 자식들인 고추와
딸기가 티격태격 싸우면 뜯어 말리기도 하고, 딸기는 방 청소하고, 고추는 유리창 닦기를 시키라고 하는 것과
5
같습니다. 하지만 옆집에 있는 어머니와 직접 얘기하지 못하기 때문에 전화를 이용한다든가 하는 점에서는
통신하는 것과 비슷할 것입니다. 그러나 응용 프로그램의 범위를 벗어난 통신은 불가능합니다. -_-. C#은
AppDomain 을 통해서 응용 프로그램의 범위를 벗어난 통신이 가능합니다. 그러나 저 조차도 이 분야에
대해서는 깊이 있는 지식이 없어서 이것으로 무엇이 가능한가에 대해서는 상상조차 하지 못합니다. 예...
자체에서 응용 프로그램 도메인 범위까지 넘나들 수 있는 것은 C#밖에 없습니다. parting : Trax 님과 4baf 님의
자주 써먹는 예제지만.. 은행구좌의 금액을 읽어와서 입금한 금액을 더해서 다시 저장하는 과정이 하나의
clientBalance = Balance.getBalance();
clientBalance += deposit;
Balance.setBalance(clientBalance)
근데, 실제로 은행에서는 이 일들이 하나가 아니라 수십개 이상 독립적으로 구좌를 갱신할거라는 거죠. 그러니
스레드가 거의 동시에 같은 데이터를 수정하다가 그 데이타 객체가 행여나 잘못되면 망한다는 거죠 -_-.;; 예를
들면, 하나의 구좌에 입금을 하고 있는 와중에 다른 사람이 다른 은행원에게 같은 구좌에 또 입금을 원한다고
팻말을 붙여 놓겠죠...아마도.. 컴퓨터도 마찬가지로 객체를 잠궈버려서 접근을 막는 식으로 작동을 합니다.
돌린다거나 이렇게 하면 된다는 거죠.. 자바에서 스레드는 표준 클래스로 정의되어 있답니다. 스레드 객체를
String word;
6
int delay;
word = WhatToSay;
delay = delayTime;
try{
for(;;) {
System.out.print(word+"");
sleep(delay);
} catch(InterruptedException e){
return;
PingPong 이라는 스레드형을 정의합니다. 루프를 돌리면서 delayTime 동안만 대기합니다. 출력을 하면
핑퐁핑퐁 그러는데 갈수록 퐁의 빈도가 낮아질겁니다. 딜레이 타임이 다르니까... 쓰레드 객체를 만든 다음에
start 하면 쓰레드 인스턴스를 만들고.. run 메쏘드를 호출해서 쓰레드가 작동한답니다.. 이외에도 동기화, 즉
7
class Account {
balance = inintialDeposit;
return balance;
balance += amount;
Interface 를 참조해서 구현하는 것도 있습니다. 자바에서 제공하는 것은 스레드 보안과 디버깅 정도입니다.
Trax : parting 님의 멀티 스레딩과 Java 에서의 처리에 대한 얘기였죠. 저는 하던 업무가 주문, 재고, 창고... 이
채운다고 하지요. -_-.). 하지만 이 락은 간단히 쓰기는 편한데... 많아지면... 복잡하고 엄청난 노가다가 됩니다.
합니다. 반면에 C#은 코드 블록을 적절히 묶어서 처리할 수 있도록 해줍니다. 이러한 것을 위해서 모니터
클래스를 제공하지요... -_-. 즉, 정수값을 바꾸는 정도가 아니라 문자열을 갖고 있든지 뭐든지간에 해당
블록안의 값들의 변경에 대한 락을 알아서 지원하는 겁니다. 단지, Monitor.Enter()와 Monitor.Exit() 만으로
말이지요. -_-. (얼마나 황당하고 편합니까... 써드 파티에서는 몇 백만원씩 받으면서 이런걸 판매하고 있는데...
8
OS 에서 빌려와서 할래?(하는 거에 비하면 프로그래머가 해야 할 게 너무 많음.) 아니면 Java 나 C#은
자체적으로 지원하니까 쉽거든 이거 할래? 라는 문제가 되겠죠. 언어를 선택할 수 있는 상황이라면 Java 나
C#을 선택할 겁니다. 4baf: 코드 블록을 묶는다는 말에 있어서 물어볼게 있어요. 자바도 메소드 단위로
synchronized 하는 것 말고
class ThreadDemo {
이것저것하기...
synchronized(aObject) {
동기화 부분
쓰면 스레드 동기화까지 알아서 되는데, 어플리케이션 개발자 입장에서 그런 컨설턴트가 필요한가요? Trax :
스레드에서의 동기화 관점에서라면... -_-. C#과 Java 모두 Interlocked 클래스를 사용합니다. 하지만 이것은
지저분해 지겠지요. -_-. 그래서 이보다 한발 더 나아가서... Lock 객체를 통한 Lock 을 지원합니다. 즉,
try
9
{
lock(this)
temp++;
Thread.sleep(1000);
counter = temp;
} // end of lock
} // end of while
} // end of try
catch ( ThreadInterruptedException)
} // end of catch
이 정도와 같을까요? 단순히 lock() 만으로 가능해집니다. Java 의 synchronized 와 비슷하지요. 하지만 보다
복잡한 프로그램에서 보다 정교하게 자원을 제어할 필요가 있다면 이것만으로 충분할까요? (저는 Java 가
스레드를 지원해도 이 부분에서는 기존의 언어들이 OS 에서 빌려오는 것 못지않게 복잡하고 어렵다고 생각하고
a1, a2 와 같은 작업이 기다리고 있습니다. 그러면 보통의 경우에는 a1, a2, t1 의 순서로 스레드가 쌓입니다.
되고, 거의 모든 스레드는 여기서 작업을 기다리게 됩니다. 미들 티어에서 이렇게 된다면 어떻게 될까요?
10
처리한 다음에 다시 돌아와서 작업을 처리할 것인지를 제어할 수 있어야합니다. 첫 번째 순차작업만을 하던
사람은 스레드를 접하면 작업 시간을 단축할 수 있다는 데에 매료되고 적용하게 됩니다. 그러나 시간이 지나면
이러한 문제점이 하나씩 나타나게 됩니다... 심지어는 평소에는 스레드간의 이동 작업과 같은 부하가 미미한
작업조차도 동시에 엄청나게 생성되는 스레드 숫자로 인해서 시스템이 반응조차 하지 않을 정도로 죽어버리고,
대부분의 CPU 사이클를 스레드의 작업 처리가 아니라 스레드의 변환작업에 소비하게 되는 현상을 겪게 됩니다.
모니터는 동기화에 있어서 동기화를 할 부분과 하지 않을 부분을 프로그래머가 결정할 수 있도록 해줍니다.
또한, 다른 코드 영역이 해제가 될 때까지 기다릴 수 있도록 해줍니다. 일종의 smart lock 의 개념이
모니터입니다. 모니터를 사용할 수 없으면, 모니터가 보호하고 있는 객체가 사용중이 됩니다. lock() 대신에
제어할 수 있다는 거겠지요. -_-. 현재 Ultra Editor 등을 보면 파일이 외부에서 변경된 것을 알아냅니다. 심지어
삭제된 것도 알아내고 질문을 합니다. 이것이 의미하는 것은? 즉 스레드의 처리입니다. 파일의 처리자체가 멀티
태스크 OS 에서는 중요한 문제가 되는 것입니다. -_-. 옛날 버전의 Ultra Editor 가 이런 것을 알았을까요?
모릅니다. -_-. 어떤 스레드가 파일을 열려고 하고, 어떤 스레드는 파일을 쓰려고 한다. 그러면 이도 저도
못하고 교착상태에 빠지겠지요? 어쨌거나 이러한 것을 간단하게 할 수 있다는 것입니다. Java 에 비하면 더
세세하게 제어할 수 있다고 생각합니다. 물론, 모니터까지 사용하는 사람은 많지 않을 겁니다. 지금까지는
생각입니다. 객체 지향 언어에서 자주 부딪히는 문제에 대해서 다음과 같은 디자인 패턴으로 스레드 동기화
문제를 다룰 것입니다.
• 상호 배제(mutual exclusion) 데이터나 코드와 같은 시스템 자원이 동시에 둘 이상의 스레드에 의해서
11
• 경계적 대기(alertable wait) 위에 제가 예로 든 것입니다. 일반적으로 스레드는 자원이 사용 가능할
사용해야 합니다. 제한된 철학자만 식사를 하는 식으로 루프를 돌면 엄청나게 많은 불필요한 CPU
생산자는 최대한의 데이터를 생산하고, 소비자는 최대한 데이터를 소비해야 합니다. 그리고 이들
생산자와 소비자는 전혀 별개의 독립되어 있어야 합니다. 그렇지 않다면 생산자가 데이터를 생산하는
동안 소비자는 아무것도 못하고, 소비자가 데이터를 소비하는 동안 생산자는 아무것도 생산하지 못하게
될 것입니다. (반대로 생각하면 소비할게 없어서 소비자가 놀고, 생산할게 없어서 생산자가 논다고
생산했다, 얼마를 소비했다.. 소비할 데이터가 얼마가 필요하다 등등.) 이것은 단일 생산자와 단일
자자... 스레드에서 이 정도 디자인 패턴이 '기초'에 해당합니다. 더 깊이 들어가 볼까요? 객체와 스레드의
동기화의 문제가 생깁니다. 내부에서 동기화하고, 클라이언트(즉 객체를 이용하는 어플)에서는 동기화에 관여를
못하게 할 것인가(내부 동기화), 아니면 객체에서 동기화를 제공할 것인가(외부 동기화)의 문제가 있고 이것
분할하여 처리할 것인가라는 문제가 있고, 저장소는 정적 스레드 로컬 저장소를 사용할 것인가, 동적 스레드
로컬 저장소를 사용할 것인가...(즉, 정정 TLS 와 동적 TLS 문제로 나뉘게 됨) 그리고 이러한 것들을 뛰어넘어
스레드 풀을 구현하게 됩니다. 보통의 경우에 이러한 스레드 풀은 MTS 나 COM+에서 제공하게 됩니다. 그리고
이것은 CLR 에서도 마찬가지입니다. 스레드 풀의 구현이 필요 없습니다. 그러나 만약 스레드풀의 구현이
12
C++로 개발하게 되는 상황이라면? ) 멀티 스레드를 지원하는 DLL 의 작성과, 멀티 스레드 인터페이스까지 갈
수 있겠지요. (즉, 작업이 많아도 화면 반응이 좋고, 빨리 빨리 동작하는 것처럼 보이게 만드는 거겠지만) 멀티
않습니다.) 이제 개발이 종반에 이르면 어떤 문제가 생길까요? 멀티 스레드 응용 프로그램은 디버깅이 상상을
어렵지요... 그에 비하면 C#은 상당히 쉬워서... 그 정도의 고민은 안해도 됩니다. ^^ 하지만 그래도 고민은
한다는 거지요. A 라는 작업을 동시에 처리하는 스레드가 10 개 있는데 그 중에 하나가 문제를 일으킨다면?
이러한 많은 문제들이 발생하게 되고, 프로세스와 스레드에 대해서 자세히 알지 않고 응용 프로그램에 적용하게
되면 위와 같은 문제들이 동시에 발생하기 때문에 스레드는 어려운 것이라는 인식이 있습니다. 4baf: Trax 님
이런 식으로 구현해야만 해요. 그리고 이 메소드가 던지는 예외는 쭉 위로 올라가다가 어느 시점에서든 반드시
catch 가 되어야 합니다. 안그러면 컴파일타임 에러죠. 제가 C#은 catch()를 반드시 할 필요가 없던 것
같은데요. 그렇다면 어떤 예외를 던져지는지 명확한 검사가 이루어지지 않고 나중에 문제발생 소지도 커지는 것
같은데, 어떻게 생각하세요. Trax 님께서 말씀하신 그 예외처리의 강력함이 자바와 비교하여 어느 것이 더
짜볼께요.
try
13
{
synchronized(this)
temp++;
Thread.sleep(1000);
counter = temp;
} // end of lock
} // end of while
} // end of try
} // end of catch
'모니터'란 용어의 정확한 의미는 모르지만 자바역시 C# 과 거의 다르지 않게 구현할 수 있습니다. 위처럼요.
클래스가 있다는 것은 알고 있습니다. 자바에는 그게 부족하죠. 하지만 기본적인 모니터는 위처럼 지원한답니다.
클래스들을 지원해달라고 요청도 하구요. Trax: 4baf님이 물어보신 별도의 책은 없습니다. 디자인 패턴에 대한
것들은 parting님이 잘 아시기 parting님께 물어보도록 하죠. 참고가 될지는 모르겠지만 『Win32 멀티스레드
14
디자인 패턴은 Java가 비교적 광범위하게 되어 있습니다. Java가 전문이시니 Java 관련서로 보는 게 좋을 것
같습니다. 쉽게 볼 수 있는 거로는 『VIsual Basic Design Patterns』가 있습니다. 4baf님이 얘기하신 것처럼
C#은 throw를 반드시 명시할 필요는 없지만 C#도 throw를 지원합니다. Java의 경우도 C#과 비슷하지만 모든
처리의 목적은 어떤 것에서 예외가 발생했는가를 알아내기 위한 것일 뿐입니다. 다시 말하면, 여기서 걸러지지
>
즉, 위의 Java 코드와 같이 throws new 를 장황하게 써가면서 선언문을 길게 하지 않아도 됩니다. 중간에
try {
if ()
catch ( InvalidCastException e)
{}
catch ( Exception e)
{}
15
이와 같이 예외를 던질 수도 있고, 다른 예외를 처리할 수도 있습니다. 이것은 C# 코드입니다. Exception 은
try
synchronized(this)
temp++;
Thread.sleep(1000);
counter = temp;
} // end of lock
} // end of while
} // end of try
} // end of catch
'모니터'란 용어의 정확한 의미는 모르지만 자바역시 C# 과 거의 다르지 않게 구현할 수 있습니다. 위처럼요...
16
메소드들은 java.lang.Object 에 정의되어 있으며 Object 클래스는 C#의 object 처럼 모든 클래스의
parent 입니다. 모든 클래스가 이용가능하죠. 위 코드는 제가 C#에서 작성한 lock()에 대해서 4baf 님이 Java 로
있습니다. -_-. Java 에서 mutex 클래스를 구현한 것도 있다고 하셨는데, 사실은 mutex 뿐만 아니라 스레드와
알겠습니다. Java 의 synchronized 가 C#의 lock 이라 생각하면 됩니다. Java 에는 모니터 클래스라는 게 따로
있지는 않습니다. 제가 말한 C#의 exception 단점은, 그 exception 을 반드시 catch 하지 않아도 된다는
것입니다. 자바는 반드시 메소드내에서 발생한 에러를 catch 하거나 혹은 throws 를 씀으로써 catch 하지
않겠다고 명시해야 합니다. public static void main() 메소드 내에서는 throws 를 명시할 수 없고, throws 로
선언된 메소드들을 호출한 경우 반드시 catch 해야 합니다. 즉 프로그램내에서 어디선가 exception 이 반드시
예외하나 catch 안했다가 시스템 exit()될겁니다. 자바의 쓰레드에 관한 제 두개의 코드중 첫번째 aObject 를
synchronized 시킨 경우는 aObject 에 대한 모니터를 획득했고, 두번째 코드는 this 인스턴스에 대한 모니터를
끝나는)을 실행중인 쓰레드가 있는 동안은 synchroznied 블럭 앞에서 대기하게 됩니다. 이런 면에서 C#의
lock 과 동일합니다. Nuthack : perl 의 멀티 쓰레드 예제 입니다..-_-.;; (Trax : 드디어 등장하셨군요.. ^^)
use Thread;
$t1->join;
$t2->join;
17
while ( 1 )
sleep 1;
sub start_sub
my $tid = Thread->self->tid;
while ( 1 )
sleep 1;
패턴 예제들이 PDF 문서로 들어있더군요. 저는 디자인 패턴이 심각하게 필요한 사람이 아니라서 사실 별
관심은 없습니다. 스몰토크 공부하다가 우연히 봤는데 Trax님 말을 들어보니 '아하, 저게 그렇게 중요한
거였구나~'하는… -_-;; 저는 디자인 패턴을 그냥 빵굽는 틀로밖에 생각을 안하기 땜에.. -_-; 제 목표는
빵공장이 아니라 집에서 구워먹는 맛있는 빵인 관계로 ^^;; Nuthack : 상호 배제(mutual exclusion)라는 게
뮤텍스가 아닌가요? Trax : 예, 상호 배제(mutual exclusion)가 뮤텍스(mutex) 맞습니다. 이것도 디자인 패턴을
프로그래머들도 많습니다. 일반적으로 OS가 제공하는 뮤텍스는 자신들의 API에 적합하게 구현한 것이지요.
18
이것은 경쟁 조건에 있는 자원들에 대해서 루프를 돌면서 순차적으로 처리하느라 CPU 자원을 소모하는 대신에
일정 개수의 토큰을 발급하는 것입니다.. 식사하는 철학자 문제도 역시 이러한 토큰의 발급... 즉, 토큰을
얘기와 디자인 패턴, 그리고 Java 와 C#에서 구현하는 스레드에 대해서 좋은 얘기를 나눈 것 같습니다. 부디
논의자 4baf - OCP, Java Programmer 이며, 현재는 C#으로 닷넷 환경에서 웹 서비스를 이용한 솔루션 개발
parting - TFT 팀 팀장이며, 주요 관심사는 스몰토크와 디자인 패턴이며 현재는 닷넷 웹 서비스 솔루션을 위한
설계를 하고 있음. Trax - Solaris System Administrator 로 일했으며, EIP 솔루션 개발을 했으며, 소일거리로
관심을 갖고 있는 분야는 C#이며 프로그래밍 이론 전반에 대해 관심을 갖고 있다. 현재는 무위도식중. Nuthack
Python, Ruby, Perl 과 같은 스크립트 언어를 다룬다. Dicajohn - 삼성 소프트웨어 멤버십 회원이며, Java 와
모바일 프로그래밍이 주요 관심사다. 이번 기사 '스레드, 그리고 Java 와 C#'는 앞으로 우리나라를 이끌어갈
내용입니다.
19
C# 쓰레드 이야기: 1. 쓰레드는 무엇인가?
'멀티 OS'라고 한다. 이것의 의미는 동시에 여러 가지 작업을 한다는 것을 뜻한다. MP3 를 들으며 워드를
작성하면서 인터넷 서핑을 할 수 있다. 이때 각각의 응용 프로그램은 하나의 프로세스를 갖는다. 그러니까 MP3
프로세스라는 뜻이다.
디스크에 저장하고 있고, 내용을 프린터에 출력하고 있고, 입력하는 동안에 맞춤법
작업에 대해서 쓰레드에게 처리를 맡기고, 다른 일을 계속해서 처리할 수도 있는 것이다. 다음 그림을 보도록
하자.
20
그림에서처럼 하나의 프로세스에서 처리해야하는 세 가지의 작업 A, B, C 가 있고 각각의 처리시간이 위의
길이와 같다고 할 경우에 첫번째와 같이 순차적으로 처리하는 경우보다는 두 번째와 같이 쓰레드를 이용하여
SimpleThread.cs
using System;
using System.Threading;
t.DoTest();
21
Thread t1 = new Thread(
new ThreadStart(WorkerThreadMethod) );
Console.WriteLine("쓰레드를 생성");
t1.Start();
Console.WriteLine("WorkerThreadMethod 시작했음");
using System;
using System.Threading;
스페이스를 선언하기만 하면 된다. (아… 깜빡한 사실이 있는데, C#에서는 쓰레드를 지원한다. Java의 쓰레드와
비슷하다고 생각하면 된다. C#과 Java의 쓰레드 지원에 대한 논의는 쓰레드, 그리고 Java와 C#을 참고하기
바란다)
22
{
t.DoTest();
Tester 라는 임시 클래스를 만들었고 Main() 함수를 선언했다. 여기서는 Main() 함수에 직접 함수를 두지 않고
DoTest()와 같은 별도의 함수를 만들어서 처리하고 있다. Main()은 응용 프로그램의 진입점(entry point)으로
new ThreadStart(WorkerThreadMethod) );
여기는 쓰레드를 생성하는 부분이다. t1 이라는 쓰레드를 생성하고, 쓰레드에서 호출할 함수는
t1.Start();
쓰레드의 시작을 요청한다. Start() 함수를 만나기 전까지 쓰레드는 시작되지 않는다는 것도 알아두자.
Console.WriteLine("WorkerThreadMethod 시작했음");
쓰레드가 사용하는 함수를 정의한 부분이다. 위에서 알 수 있는 것처럼 쓰레드에 의해서 호출되는 메소드는
반드시 void 형으로 정의해야 한다. 다음에는 여러 개의 쓰레드를 동시에 처리하는 멀티쓰레드에 대해서
23
C# 쓰레드 이야기: 2. 다중 쓰레드
쓰레드를 이용하는 프로그램을 작성하는 것은 쉽다. 원하는 수 만큼 쓰레드를 생성하는 프로그램을 작성하기만
하면 된다. 여기서는 하나의 응용프로그램이 세 개의 쓰레드를 갖고 있고, 각각의 쓰레드는 인쇄하기, 저장하기,
namespace csharp
using System;
using System.Threading;
class MultiThreadApp
app.DoTest();
그리고 실제 코드는 DoTest() 함수에서 처리하기 때문에 Main()에는 간단한 코드만이 필요하다.
new ThreadStart(DoPrinting) );
24
Thread t2 = new Thread(
new ThreadStart(DoSpelling) );
new ThreadStart(DoSaving) );
t1.Start();
t2.Start();
t3.Start();
먼저 각각의 쓰레드 t1, t2, t3 를 만들고, 각 쓰레드에는 DoPrinting, DoSpelling, DoSaving 메소드를 위임한다.
쓰레드의 시작을 위해 각 쓰레드의 Start()를 호출한다. 이제 각각의 쓰레드를 시작하도록 했으므로 각각의 함수
Console.WriteLine("인쇄 시작");
Thread.Sleep(120);
Console.Write("p|");
Console.WriteLine("인쇄 완료");
먼저 쓰레드를 이용하는 메소드는 void 타입이어야 한다고 설명했었다. 즉, 반환값을 갖지 않는다. 인쇄 시작을
의미하고, 진행중임을 알기 위해 여기서는 루프를 돌 때마다 p|을 출력한다. 그리고 모든 작업이 끝나면 '인쇄
처리하고 있음을 설명하기 위해 Thread.Sleep()의 시간을 각각 다르게 지정했다. 전체 소스는 다음과 같다.
25
<B<MULTITHREADAPP.CS< b>
namespace csharp
using System;
using System.Threading;
class MultiThreadApp
app.DoTest();
new ThreadStart(DoPrinting) );
new ThreadStart(DoSpelling) );
new ThreadStart(DoSaving) );
t1.Start();
26
t2.Start();
t3.Start();
Console.WriteLine("인쇄 시작");
Thread.Sleep(120);
Console.Write("p|");
Console.WriteLine("인쇄 완료");
Console.WriteLine("철자 검사 시작");
Thread.Sleep(100);
Console.Write("c|");
Console.WriteLine("철자 검사 완료");
27
}
Console.WriteLine("저장 시작");
Thread.Sleep(50);
Console.Write("s|");
Console.WriteLine("저장 완료");
작업(t2), 철자 검사(t3)를 시작한 것을 알 수 있다. 그러나 각각의 작업을 뜻하는 알파벳을 출력하도록 한
결과를 유심히 살펴보면 s|c|s|p|s|c 와 같이 각각의 작업이 뒤죽박죽으로 섞여서 동시에 처리되고 있다는
28
것을 알 수 있을 것이다(Thread.Sleep() 함수에 각각 다른 지연 시간을 지정했기 때문에 이와 같은 결과를
탐구하고 싶다면 각 Do 함수의 루프 카운터와 Thread.Sleep()의 지연 시간을 바꿔가면서 테스트 해보기 바란다.
다음은 위 예제에서 각각의 쓰레드를 생성해서 처리하던 것을 쓰레드의 배열로 만들어서 보 다 간결하게
Thread[] aThread =
};
t.Start();
다음 단계 다음 시간에는 쓰레드를 종료하는 방법과 예외처리, 백그라운드 처리와 같은 방법에 대해서 알아볼
많은 것들을 살펴보고자 한다면 온라인 도움말을 찾아보기 바란다. 궁금한 사항이 있다면 게시판이나
29
C# 쓰레드 이야기: 3. 쓰레드 제어
지난 시간(2. 다중 쓰레드)에는 여러개의 쓰레드를 생성하는 방법에 대해서 간단히 살펴보았다. 생각처럼
어렵지 않았을 것이다. 단순히 여러개의 쓰레드를 생성하고 각각의 함수를 위임하고 실행하면 그만이었을
것이다. 쓰레드 기다리기(Joining Thread) 일반적인 쓰레드 처리는 코드의 수행과 상관없이 계속해서 실행된다.
만약, 여러분이 쓰레드를 이용해서 긴 시간 동안 처리해야 하는 작업을 맡았고, 그 작업이 끝나면 결과를
받아서 화면에 출력을 하거나 계산을 하는 프로그램을 작성한다고 생각해보자. 이 경우에 쓰레드에서 처리하고
있는 작업이 끝나기도 전에 프로그램은 이미 출력이나 계산을 수행하는 코드를 실행하게 된다(아마도 에러를
만나게 될 것이다). 이러한 경우에 화면에 출력하거나 계산을 수행하기 전에 쓰레드가 끝나는 것을 기다려야
한다.
Thread[] aThread =
{
C# 에센스
new Thread( new ThreadStart(DoPrinting) ),
};
t.Start();
30
aThread[1].Interrupt();
또한 모든 Do 함수에서
Thread.Sleep(50);
것이다.
코드가 쓰레드로 수행되고 있기 때문에 실제로 모든 쓰레드가 종료되지 않았는데 '모든 쓰레드가
t.Start();
aThread[0].Join(); // 인쇄 작업
aThread[1].Join(); // 철자 검사
aThread[2].Join(); // 저장 하기
31
Console.WriteLine("모든 쓰레드가 종료되었습니다");
foreach 루프문에서 Join()을 수행하지 않고, 바깥에서 쓰레드의 배열에서 직접 Join()을 수행한다는 것에
주의하기 바란다. 각각의 쓰레드는 인쇄 작업, 철자 검사, 저장 하기를 수행한다. 또한 모든 쓰레드가 종료될
이와 같이 각각의 쓰레드가 독립적으로 처리되고, 모든 쓰레드가 종료된 다음에 특정 코드를 실행하기 위해서
t2.Join();
과 같은 코드를 넣으면, 쓰레드 t1 에서 수행중인 함수의 실행을 중지하고, 쓰레드 t2 가 종료될 때 까지 실행을
기다리게 된다. 위에서는 aThread[0], aThread[1], aThread[2]에 대해서 각각 Join()을 호출하였다. 각 쓰레드
프로그램 쓰레드는 실행을 중지하고 각각의 aThread[0], aThread[1], aThread[2]의 실행이 종료될 때까지
첫번째 시간에 응용 프로그램의 프로세스와 쓰레드에 대해서 이야기 한 것을 기억하는가? 기억하지 못하는
독자를 위해서 잠시 기억을 되살려 보도록 하자. 응용 프로그램은 하나의 프로세스를 가진다. 이 프로세스는
실제로 하나의 쓰레드이며, 프로세스는 어드레스 공간에 대한 단순한 주소라고 이야기 했었다. 또한 프로세스는
32
단순히 쓰레드에 대한 컨테이너 역할을 한다고 얘기했었다. 다시 말해 각각의 aThread[].Join()은 응용
쓰레드에 대해서 Join()을 했으므로 모든 쓰레드가 종료될 때까지 응용 프로그램 쓰레드 Main()을 실행을
쓰레드에 관계없이 동시에 실행되기 때문이다. Join 에서 주의할 점 위에서 Join 이 어떻게 작동하는 가에
대해서 자세하게 설명했다(어쩌면 독자들의 머리를 아프게 했을지도 모르겠다). 여러분은 여러 개의 쓰레드를
foreach 를 사용하여 멋지게 처리하는 것을 보았는데, 왜 루프안에 Join()을 넣지 않았는지 의아해 했을지도
모르겠다. '왜 루프안에서 Join()을 수행하지 않고, 루프 밖에서 일일이 수행하는 거지?' 이제, 이것에 대해서
t.Start();
t.Join();
33
Console.WriteLine("모든 쓰레드가 종료되었습니다");
여러분이 생각하는 것처럼 foreach 루프안에서 일괄적으로 쓰레드를 시작하고 Join()을 수행하고 있다. 코드를
루프안에서 Start()에 의해서 쓰레드가 시작되고, Join()에 의해서 응용 프로그램 쓰레드에 대해서 Join()을
수행하게 된다. 다시 말해, 응용 프로그램은 t.Join();까지 실행한 다음에 쓰레드 t 의 수행이 끝날 때까지 모든
작업을 중지하고 기다리게 된다. 마찬가지로 두 번째 루프를 처리할 때도 같은 방식으로 동작한다. 때문에
t.Start();
t.Join();
34
쓰레드를 시작하는 부분과 쓰레드를 Join()하는 부분을 각각 두 개의 루프로 분리해서 처리하도록 한다. Join
대기하기 먼저 각각의 쓰레드 t1, t2, t3 가 있다고 하자. 다음과 같이 모두 Join()을 수행하고 있다고 가정하자.
t1.Join();
t2.Join();
t3.Join();
이 세가지 쓰레드는 각각의 쓰레드가 종료되고 제어권을 다시 넘길 때까지 응용 프로그램 쓰레드를 중지시킨다.
이러한 경우에 일정 시간 동안만 응용 프로그램 쓰레드를 대기 시키도록 할 수 있다. 사용법은 다음과 같다.
t2.Join(100);
t3.Join(100);
t.Start();
aThread[0].Join(100);
aThread[1].Join(100);
aThread[2].Join(100);
35
결과에서 알 수 있는 것처럼 각각의 쓰레드가 Join(100)에서 지정했으므로 300ms 초 이후에 "모든 쓰레드가
각각의 쓰레드에 대해서 Join(1000)으로 변경했으므로 3000ms 이후에 응용 프로그램 쓰레드에 제어권이
프로그래머가 쓰레드를 직접 정리하지 않아도 된다. 정리 마지막으로 쓰레드에 대해서 정리해 보도록 하자. 위
36
쓰레드가 갖고 있다. 벌써 머리가 복잡해 지는가? 이제 잠시 쉬면서 다음을 위한 휴식을 하도록 하자. 다음
Thread.Sleep()을 이용해서 쓰레드의 처리를 단순히 지연시키는 것에서부터, 쓰레드의 처리가 종료될 때까지
System.Threading.Thread 클래스에 대해서 자세히 살펴보는 시간을 갖도록 하자. 아무쪼록 다음 시간까지
37
C# 쓰레드 이야기: 4. 쓰레드 기본 개념
By 한동훈 필자는 쓰레드에 대해서 글을 쓰지만, 전혀 쓰레드에 대한 전문가가 아니라는 사실을 독자들이
라고 얘기하곤 한다. 그래서 이번에는 실제 코드와는 관계없이 쓰레드의 기본 개념에 대해서 설명하고자 한다.
그러니 윈도우에서 이렇게 쓰레드를 처리한다라고 생각하지 말 것을 당부한다. 쓰레드 스케줄링 윈도우 2000 의
것이 시분할(Time Sharing)에 의해서 가능하다고 한다. 참고: 정확히 말하면 NT 운영체제는 시분할이 아닌 타임
운영체제의 스케줄러다. 스케줄러는 어떤 쓰레드를 다음에 동작시킬 것인지를 결정한 뒤에 선택한 쓰레드를
순위를 결정하는 쓰레드 우선 순위가 있다. 대부분의 운영체제에서는 우선 순위를 결정하는데 라운드
38
들어가며, 스케줄러는 가장 우선 순위가 높은 쓰레드를 차례대로 수행한다. 스케줄러는 이 큐의 쓰레드를
된다. 쓰레드의 우선 순위는 숫자로 표시하며, 이 숫자값은 운영체제마다 다르다. 윈도우 2000 의 작업
있다.
그림에서 볼 수 있는 것처럼 윈도우 2000 서버는 실시간, 높음, 보통 초과, 보통, 보통 미만, 낮음과 같은
프로세스 우선 순위를 설정할 수 있다. 윈도우의 우선 순위는 32 를 기준으로 설정한다. 이 숫자값이 높으면
39
실시간(Real Time) 24 REALTIME_PRIORITY_CLASS
휴지(Idle) 1 THREAD_PRIORITY_IDLE
[표 1] Win32 와 CLR 우선순위 표에는 Win32 와 CLR 의 우선 순위를 각각 정리해 두었다. 표에서 알 수 있는
것처럼 닷넷 환경에서는 다섯 가지의 우선 순위만을 지정할 수 있으며, 시스템의 수행에 결정적인 영향을 줄 수
각각의 숫자값은 운영체제마다 다르다. [표 1]의 Win32 플래그는 winbase.h 헤더파일에 정의되어 있다.
하자. 이 경우에 검색을 수행하는 쓰레드와 사용자 인터페이스를 담당하는 쓰레드가 있어야 한다. 검색을 하는
도중에 사용자가 검색을 중지하고자 '중지'버튼을 클릭한다면 바로 반응을 보일 수 있어야 한다. 이 경우에는
한다. 닷넷에서의 쓰레드 우선 순위 닷넷에서 쓰레드 우선 순위를 변경하여 프로그램이 어떻게 동작하는지
알아보도록 하자. 먼저 지난 시간에 이용한 예제를 다시 한 번 살펴보도록 하자. 여기서는 Join()문을 없앴으며
이름: MultiThread.cs
namespace csharp
40
{
using System;
using System.Threading;
class MultiThreadApp
app.DoTest();
} // End of Main()
Thread[] aThread =
};
t.Start();
} // End of DoTest()
41
private void DoPrinting()
Console.WriteLine("인쇄 시작");
Thread.Sleep(50);
Console.Write("p|");
Console.WriteLine("인쇄 완료");
} // End of DoPrinting()
Console.WriteLine("철자 검사 시작");
Thread.Sleep(50);
Console.Write("c|");
Console.WriteLine("철자 검사 완료");
} // End of DoSpelling()
42
Console.WriteLine("저장 시작");
Thread.Sleep(50);
Console.Write("s|");
Console.WriteLine("저장 완료");
} // End Of DoSaving()
결과에서 볼 수 있는 것처럼 저장, 인쇄, 철자 검사를 시작하고 차례대로 저장, 인쇄, 철자 검사 순으로
someThread.Priority = ThreadPriority.Highest;
43
ThreadPriority 는 열거형(enum)이고, 사용할 수 있는 종류는 [표 1]에 있는 것과 같다. DoTest() 함수를 다음과
같이 바꿔보도록 하자.
Thread[] aThread =
};
aThread[0].Priority = ThreadPriority.Lowest;
aThread[1].Priority = ThreadPriority.Highest;
44
위에서 알 수 있는 것처럼 각각의 쓰레드에 대해서 쓰레드 우선 순위를 지정할 수 있다. [표 1]에서 알 수 있는
것처럼 CLR 에서는 5 가지의 우선 순위를 지정할 수 있다. CLR 은 현재 MS 의 윈도우에서만 실행되지만 다른
쓰레드가 우선 순위를 가지게 될까? 이 경우에는 어떤 쓰레드가 우선 순위를 갖는 다고 보장할 수 없다. 이것은
실행된다고 보장하지 않는다. 같은 순위의 쓰레드를 차례대로 실행할 수도 있고, 무작위로 실행할 수도 있다.
같은 언어에서 쓰레드 프로그래밍을 한 경험이 있다면 이상하다고 여기는 부분이 있을 것이다. 지금까지 필자가
살펴보아도 각각의 쓰레드 우선 순위가 Highest, Normal, Lowest 인데도 불구하고, 실행 순서와 종료 순서가
쓰레드가 사이좋게 제어권을 양보하면서 차례대로 실행될 수 있는 것은 각각의 루프안에 있는 Sleep() 때문이다.
지우고, 루프의 횟수를 2000 회로 늘렸다(이와 같이 루프를 늘리는 것은 시스템에 따라 정상적으로 쓰레드
이름: Selfish.cs
namespace csharp
45
using System;
using System.Threading;
class MultiThreadApp
app.DoTest();
Thread[] aThread =
};
aThread[1].Priority = ThreadPriority.Normal + 2;
t.Start();
46
foreach( Thread t in aThread)
t.Join();
Console.WriteLine("인쇄 시작");
Console.Write("p|");
Console.WriteLine("인쇄 완료");
Console.WriteLine("철자 검사 시작");
Console.Write("c|");
47
Console.WriteLine("철자 검사 완료");
Console.WriteLine("저장 시작");
Console.Write("s|");
Console.WriteLine("저장 완료");
제대로 확인할 수 없다고 의심할 수도 있을 것이다. -_-; 그러면 위 코드에서 다음 부분을 주석 처리하고 다시
// aThread[1].Priority = ThreadPriority.Normal + 2;
실행 결과는 각각의 쓰레드가 비슷하게 시작하고, 비슷하게 종료된다. 만약 파일을 검색하는 프로그램을
만든다고 가정하자. 이때 파일을 검색하는 쓰레드에 Sleep() 구문이 없다면 화면에 버튼을 클릭하려해도 반응을
보이지 않을 것이다. 특정 쓰레드가 자원을 독점하지 않도록 하려면 쓰레드 루프안에 Thread.Sleep()이
들어가야 한다. 이제 각각의 Do 함수를 다음과 같이 수정하고 다시 컴파일하고 실행해 보도록 하자.
48
{
Console.WriteLine("인쇄 시작");
Console.Write("p|");
Thread.Sleep(1);
Console.WriteLine("인쇄 완료");
Console.WriteLine("철자 검사 시작");
Console.Write("c|");
Thread.Sleep(1);
Console.WriteLine("철자 검사 완료");
Console.WriteLine("저장 시작");
49
Console.Write("s|");
Thread.Sleep(1);
Console.WriteLine("저장 완료");
지워버리자. 물론, 우선 순위를 설정하는 부분이 있다면 그 부분도 지우도록 한다. DoSpelling() 함수를 다음과
같이 수정한다.
Console.WriteLine("철자 검사 시작");
Console.Write("c|");
Console.WriteLine("철자 검사 완료");
실행 결과를 보면 불행히도 쓰레드의 자원 독점은 일어나지 않는다. 각각의 쓰레드가 여전히 번갈아가며
실행되는 것을 알 수 있다. 이것은 타임 슬라이싱 때문에 그렇다. 타임 슬라이싱(Time Slicing) CPU 가 하나인
새로운 쓰레드를 스케줄링하기 위해 어떻게 쓰레드에서 CPU 의 제어권을 가져올 수 있는가? 운영체제도 결국
쓰레드에게 적절한 시간을 할당한다. 다음 인터럽트가 발생할 때까지 쓰레드는 CPU 를 독점적으로 사용하게
50
된다. 인터럽트가 발생하면 쓰레드는 스케줄러에게 제어권을 넘겨준다. 쓰레드가 제어권을 넘겨주면 스케줄러는
예제에서 같은 우선 순위를 갖고 있는 경우에는 자원의 독점적인 사용이 일어나지 않는다. 마찬가지 이유로
하나의 쓰레드의 우선 순위가 높은 경우에는 하나의 쓰레드가 계속해서 실행된다. 그러나 CLR 이 NT 가 아닌
정적 멤버 설명
GetData()
쓰레드의 현재 도메인에 대해서 실행중인 쓰레드의 특정 슬롯의 값을 설정하거나 가져온다.
SetData()
GetDomain()
현재 쓰레드가 실행중인 도메인에 대한 참조를 가져온다.
GetDomainID()
[표 2] Thread 클래스의 정적 멤버
인스턴스
설명
멤버
51
쓰레드를 죽일 때 사용한다. 이 메소드를 사용하면 ThreadAbortException 예외가
Abort()
발생한다(ThreadState.Aborted, ThreadState.AbortRequested).
상태를 구별하지는 못한다. 쓰레드의 상태를 알고자 하는 경우에는 ThreadState 를 사용하도록 한다. Abort()에
Interrupt()를 사용하도록 한다. Suspend()를 사용하는 것은 바람직하지 않으며 대신에 Sleep()을 사용하도록
수행이 보류된 스레드는 별도의 작업(Resume() 사용과 같이)이 없는 경우에는 다시 수행되지 않는다. 이러한
쓰레드를 보류된 쓰레드라 한다. 보류된 쓰레드와 블로킹된 쓰레드(Blocked Thread)의 차이점은 다음과 같다.
보류된 쓰레드는 별도의 작업이 없으면 더 이상 실행되지 않으며, 블로킹된 쓰레드는 프로그래머가 자발적으로
Sleep(), Join()과 같은 함수를 호출하여 특정 기간 동안 쓰레드의 수행을 보류하는 것이다. 따라서 지정한
기간이 지나거나(Sleep 의 경우), 특정 쓰레드의 실행이 끝난 경우(Join 의 경우)에 쓰레드는 다시 실행되게 된다.
요약 쓰레드 스케줄링과 쓰레드 우선순위에 대하여 알아보았다. 반드시 기억해야 할 것은 다음과 같다.
• 쓰레드가 독점하지 않도록 하려면 쓰레드 루프에 Thread.Sleep() 등을 사용하여 적절히 제어권을
양보하도록 해야 한다.
없다.
52
• 코드를 실행하도록 하기 위해서 우선 순위를 제어하는 것은 잘못하고 있는 것이다. 따라서
좋다.
간단히 알아볼 것이다. 또한, 쓰레드의 예외 처리에 대해서 알아보고, 어떠한 문제점이 있는지 알아보도록 하자.
참고로 필자는 쓰레드에 대해서 모르기 때문에 글이 틀릴 수가 있다. 그러니 이상한 점이 있다면 주저말고
53
C# 쓰레드 이야기: 5. NT vs UNIX
있다. 그리고 둘 다 라운드 로빈 방식에 의해서 스케줄링을 한다는 것만 기억하자. NT 와 UNIX 의 차이는 NT 는
쓰레드로 스케줄링을 한다는 것이고 UNIX 는 프로세스로 스케줄링을 한다는 것이다. NT 는 하나의 프로세스에
들어, 동시에 200 개 이상의 작업을 처리한다고 할 때, NT 에서는 200 개의 쓰레드로 하지만 UNIX 에서는
200 개의 프로세스로 처리하기 때문에 NT 에서 비교적 더 잘 처리할 수 있다. 그러나 NT 에서도 동시에 많은
작업을 처리하는 데에는 쓰레드가 무겁기에 경량화(lightweight) 쓰레드를 소개했고, UNIX 에서도 마찬가지로
UNIX 의 경량화 프로세스, 즉 쓰레드를 이용하게 된 것은 최근의 일이다. 시스템에 있는 프로세스의 확인은
수 있다. GNU 유틸리티인 top 을 이용할 수도 있다. 참고: NT 4.0 에서 소개된 경량화 쓰레드인 fiber 는 다소
생성한 쓰레드내에 있으므로 이 쓰레드의 스케줄링을 따른다. 쓰레드 프로그래밍 이점 쓰레드 프로그래밍이
54
두 개의 쓰레드를 갖고 있다면 다음 그림과 같이 될 것이다.
데이터를 주고 받으려면 IPC(Inter Process Communication)와 같은 복잡한 환경을 이용해야 한다. 기본적으로
프로세서 친밀도 시스템에 있는 모든 프로세스는 프로세서 친밀도를 갖고 있다. 프로세서 친밀도는 프로세스가
어떤 CPU 에서 실행될 것인지를 결정한다. 프로세서 친밀도는 NT 와 UNIX 프로세스 모두 갖고 있다. 참고로
윈도우 2000 부터는 하나의 프로세스에 있는 쓰레드에 대해서도 프로세서 친밀도를 지정할 수 있다. 또한
55
환경에서 시스템 부하가 많이 걸리는 작업을 특정 CPU 에 나누어서 부하를 줄이고, 처리 시간을 줄이는 데
경우에만 사용하도록 한다. NT 는 운영체제의 버전과 종류에 따라 사용할 수 있는 프로세서의 개수가 제한되어
Bit mask
Binary value
Eligible processors
0x0001
00000000 00000001
0x0003
00000000 00000011
1, 2
0x0007
00000000 00000111
1, 2, 3
0x0009
00000000 00001001
1, 4
0x007F
00000000 01111111
1, 2, 3, 4, 5, 6, 7
UNIX 에서는 mpstat 나 dmesg 와 같은 명령으로 간단히 확인할 수 있다. C#에서 간단히 현재 실행중인
프로그램의 프로세서 친밀도를 알아내는 것은 다음과 같다. 대개의 경우 자신의 CPU 가 1 개일 것이므로 값은
56
이름: ProcessApp.cs
using System;
using System.Diagnostics;
using System.ComponentModel;
class ProcessApp
try
//Process Name
//Processor Affinity
catch(Win32Exception e)
Console.WriteLine(e.ToString());
57
Process proc = Process.GetCurrentProcess();
있다.
proc.ProcessorAffinity = (IntPtr)3;
만나게 될 것이다. IntPtr 데이터형은 시스템에 따른 고유한 정수 값 형식을 지정한다. Int 형을 2 바이트로
쓴다면 2 바이트를 사용하고, 4 바이트를 사용하는 환경에서는 4 바이트를 사용하도록 하는 정수형이다. 다음은
이름: pinfo.cs
using System;
using System.Diagnostics;
class pinfoApp
// Process Name
58
// Process Id
다음 단계 프로세서 친밀도에 대해서 알아보았다. 다음에는 쓰레드의 예외 처리에 대해서 알아보도록 하자.
59
C# 쓰레드 이야기: 6. 쓰레드 예외 처리
쓰레드의 예외 처리는 여러 가지가 있지만, 여기서는 몇 가지만을 간단히 다룰 것이다. 그러니까 늘 그렇듯이
이름: ThreadEx.cs
using System;
using System.Threading;
using System.ComponentModel;
class ThreadExceptionApp
te.DoTest();
try
Thread[] myThreads =
60
new Thread( new ThreadStart(SayHappy) )
};
myThread.Start();
catch(ThreadStateException)
Console.WriteLine("Hello, everyone.");
Thread.Sleep(100);
61
for(int LoopCtr = 1; LoopCtr < 10; LoopCtr++)
Console.WriteLine("Merry~~~~~~");
Thread.Sleep(100);
Thread.Sleep(100);
Thread.Sleep(100);
인터럽트하는 Interrupt(), 다른 쓰레드의 실행을 기다리는 Join(), 쓰레드를 중지시키는 Abort()가 있다. 그리고
62
ThreadAbortException 이다. 먼저 ThreadInterruptedException 에 대해 알아보기 위해 예제에서 먼저 DoTest()
myThread.Start();
Thread.Sleep(5);
myThreads[1].Interrupt();
try
Console.WriteLine("Merry~~~~~~");
Thread.Sleep(100);
catch(ThreadInterruptedException)
63
}
쓰레드가 인터럽트되는 경우에 실제 쓰레드로 실행되는 함수에서 예외를 잡아야 한다. 이제 코드를 다시
Hello, everyone.
Merry~~~~~~
Hello, everyone.
… some code …
try
Thread[] myThreads =
64
new Thread( new ThreadStart(SayHello) ),
};
myThread.Start();
Thread.Sleep(5);
//do nothing
myThreads[1].Resume(); // ThreadStateException 예외 발생
catch(ThreadStateException)
65
Thread.Sleep(5)은 각각의 쓰레드가 실제로 시작하도록 하기 위한 약간의 지연 시간이다. 이와 같은 지연
시간이 없다면 Start() 다음에 Abort()를 호출하기 때문에 쓰레드의 상태는 Unstarted 가 된다.
myThreads[1].Abort();
SayMerry 함수를 위임한 두 번째 쓰레드를 중지시킨다. 1 부터 100 까지 루프를 실행하는 부분은 실제로
수행되는 코드를 대신하기 위한 것이다. 이제 중지된 쓰레드를 계속 실행하도록 Resume()을 호출하면 예외가
Hello, everyone.
Merry~~~~~~
Hello, everyone.
일반적으로 쓰레드의 예외처리는 SayMerry 와 같이 쓰레드에 위임되는 함수에서 처리하며, 쓰레드 상태와 같은
즉 catch 할 수 없는 특별한 예외이다. 예외가 발생하면 쓰레드를 강제로 죽이기 전에 쓰레드에 위임된 함수의
finally 블록을 실행한다. 따라서 finally 블록에서 예기치 않은 연산을 수행할 수도 있기 때문에 쓰레드의 종료를
보장하기 위해 Join()등을 호출해야 한다. Join()은 쓰레드가 실행을 멈출때까지 반환하지 않기 때문에 쓰레드를
블로킹하는 호출이다. 다시 말해서, Abort()를 사용했을 때 쓰레드의 상태가 WaitSleepJoin 이 아니면 예외가
발생하지 않는다. 이것은 실제로 Abort()를 사용할 때 미묘한 문제를 일으키게 된다. 지금부터 예제를
코드를 제거할 것이다. 먼저 각각의 Say 함수의 Thread.Sleep()을 제거하고, 각각의 Say 함수의 루프는
66
100 회씩 반복하도록 수정한다. 마찬가지로 DoTest() 함수도 수정한다. 수정된 DoTest(), Say 함수들은 다음과
같다.
try
Thread[] myThreads =
};
myThread.Start();
//do nothing
myThreads[1].Abort();
67
catch(ThreadStateException)
Console.WriteLine("ThreadStateException occured");
Console.WriteLine("Hello, everyone.");
try
Console.WriteLine("Merry~~~~~~");
catch(ThreadAbortException)
68
Console.WriteLine("------------------------");
Console.WriteLine("------------------------");
finally
Console.WriteLine("------------------------");
Console.WriteLine("------------------------");
69
먼저 DoTest() 함수에서 Abort()를 사용하기 전에 쓰레드의 상태를 출력하도록 한다. 이 경우에는 사용자마다
myThreads[1].Abort();
SayMerry()에서는 예외가 발생한 부분을 알아보기 쉽도록 하기 위해서 위와 같이 하였다. 여기서는 쓰레드를
WaitSleepJoin 으로 하는 부분을 배제했기 때문에 catch 블록은 실행되지 않고 finally 블록만 실행된다는 것에
주의하기 바란다. 코드를 실행하고 컴파일하면 다음과 같은 결과를 볼 수 있을 것이다. 다음 결과는 편의상
[생략]
Merry~~~~~~
Merry~~~~~~
------------------------
------------------------
[생략]
[생략]
결과에서 알 수 있는 것처럼 Abort()를 호출했지만, 예외가 발생하지 않았다. 이제 DoTest() 함수를 다음과
70
for(int LoopCtr = 1; LoopCtr < 1000; LoopCtr++)
//do nothing
Thread.Sleep(10);
myThreads[1].Abort();
종료되기 때문에 실제 쓰레드 상태가 Unstarted 로 프로그램이 종료되게 된다. 따라서 실제 쓰레드를 시작한
------------------------
------------------------
------------------------
------------------------
Suspend 등으로 쓰레드가 블로킹되어 있는 경우에 Abort()를 사용할 경우에는 예외를 잡을 수 있으나 그렇지
않은 경우에는 예외를 잡을 수 없다. 따라서 Abort()로 쓰레드를 죽인 다음에, 쓰레드에 위임된 함수의 finally
있도록 해야 한다(즉, 쓰레드의 상태를 WaitSleepJoin 으로 만들어서 예외를 잡을 수 있도록 한다). 여기서는
설명하지 않지만 가끔 쓰레드를 사용하다 보면 Win32Exception 을 만나는 경우가 발생할 수 있다. 그러한
71
Win32 예외 코드로 변환해준다. 요약 쓰레드의 예외 처리는 교착상태, 경쟁 조건 등과 같은 여러 가지 문제에
쓰레드의 예외 처리는 예외의 종류에 따라 위임된 함수에서 처리해야 하는 예외와 쓰레드를 이용하는 함수에서
처리해야 하는 예외가 다르므로 주의하기 바란다. 또한, ThreadAbortException 예외는 쓰레드가 WaitSleepJoin
보던 top 과 비슷한 wintop 을 C# 언어와 윈도우 폼으로 작성해 볼 것이다. 쓰레드 프로그래밍을 하다 보면
NT 의 작업 관리자의 정보로는 부족한 경우가 있다. 때문에 보다 자세한 정보를 볼 수 있는 윈도우 버전의
72
C# 쓰레드 이야기: 7. C#으로 만드는 WinTop
지금까지 쓰레드를 이용하는 방법에 대해서 알아보았고, 예외 처리에 대해서 짤막하게 알아보았다. 이번에는
이러한 지식들을 정리하고 한데 모아보는 의미에서 프로세스 모니터를 만들어 볼 것이다. NT 에서의 작업
관리자나 Linux 에서의 top 과 같은 유틸리티를 사용하여 시스템에서 실행중인 프로세스의 상태를 일목요연하게
알아볼 수 있었다. 그러나 프로세스 밑에서 실행중인 쓰레드의 정보를 얻는 것은 작업 관리자로는 어렵다. ^^;
WinTop 을 작성하는 방법은 메모장과 같은 텍스트 에디터를 사용하는 방법과 VS.NET 과 같은 개발환경을
사용할 수 있다. 어느쪽을 사용해도 되며, 여기서는 메모장만 이용한다. 여기서 작성할 프로그램의 실행 화면은
다음과 같다.
그리고 각각의 프로세스를 클릭했을 때 쓰레드에 대한 자세한 정보를 볼 수 있으며 결과는 다음과 같다.
73
이 화면은 MS 워드의 프로세스에 대한 정보를 선택한 것이다. 먼저 첫번째 화면부터 작성해보도록 하자.
이름: WinTop.cs
using System;
using System.Collections;
using System.Drawing;
using System.ComponentModel;
using System.Windows.Forms;
using System.Diagnostics;
namespace WinTop
Application.Run(new MyForm());
74
private System.Windows.Forms.Label lblProcessTotal;
#endregion
폼에서 사용할 각각의 컨트롤을 private 으로 선언하고, 몇 가지를 전역변수로 사용하기 위해 public 으로
여기서 주목할 부분은 IContainer 인터페이스를 사용한 components 이다. IContainer 인터페이스는 컨트롤에
대한 컨테이너 역할을 한다. ISite, IComponent 인터페이스의 구현이 IContainer 에 포함되어 있다. #region 과
#endregion 이라는 지시자를 사용하여 VS.NET 에서 코드를 숨기도록 할 수도 있다. 다음은 MyFrom 클래스에
75
대한 생성자를 정의하는 부분이다. 여기서는 앞에서 선언한 각 윈도우 폼 컨트롤들의 인스턴스를 생성하고,
public MyForm()
// ComponentModel 지원
//timer 초기화
timer.Enabled = true;
timer.Interval = 1000;
components 컨테이너에 timer 를 담아두며, 타이머를 설정한다. 타이머 이벤트가 발생하는 간격은
1000ms(1 초)로 설정하며, 이벤트 핸들러를 선언하여 이벤트가 발생할 때 실행할 메소드를 위임한다.
// form Init.
this.Text = "WinTop";
lstProcess.Name = "lstProcess";
lstProcess.HeaderStyle = ColumnHeaderStyle.Clickable;
lstProcess.View = View.Details;
lstProcess.FullRowSelect = true;
lstProcess.Sorting = SortOrder.Ascending;
76
다음은 폼의 제목을 WinTop 으로 설정하며, 프로세스 목록을 보여줄 ListView 클래스를 적절하게 선언하는
부분이다. ListView 는 탐색기에서 각각의 항목을 보여주는 데 사용하는 클래스이다. 주의할 것은 우리가 원하는
colHeader1.Text = "Process";
colHeader1.Width = 70;
colHeader1.TextAlign = HorizontalAlignment.Left;
colHeader2.Width = 70;
colHeader2.TextAlign = HorizontalAlignment.Left;
colHeader3.Text = "Priority";
colHeader3.Width = 70;
colHeader3.TextAlign = HorizontalAlignment.Right;
colHeader4.Width = 120;
colHeader4.TextAlign = HorizontalAlignment.Right;
77
colHeader5.Text = "Virtual Mem(KB)";
colHeader5.Width = 120;
colHeader5.TextAlign = HorizontalAlignment.Right;
colHeader6.Width = 150;
colHeader6.TextAlign = HorizontalAlignment.Left;
colHeader1,
colHeader2,
colHeader3,
colHeader4,
colHeader5,
colHeader6
});
이 부분은 ListView 에 추가할 각각의 컬럼을 추가하는 부분이다. 하나의 컬럼은 ColumnHeader 타입으로
정의해야 한다. 마지막에는 ListView 에서 프로세스를 클릭하면 해당 프로세스에 대한 쓰레드 정보를 보여주기
lblTotal.Text = "Total";
78
this.lblThreadTotal = new System.Windows.Forms.Label();
각각의 Label 을 설정하고 위치와 크기를 정하는 부분이다. 그 외 Text 속성을 정의할 때 @를 사용하였다.
lblPath.Text = "C:₩₩WinNT₩₩System32";
있다.
lblPath.Text = @"C:₩WinNT₩System32";
것이다.
// Panel Init.
panTotal.BorderStyle = System.Windows.Forms.BorderStyle.Fixed3D;
panTotal.Controls.AddRange(new System.Windows.Forms.Control[] {
lblProcessTotal,
79
lblThreadTotal,
});
다음은 체크박스를 만드는 부분이다. 체크박스는 화면에 표시되는 쓰레드 정보를 갱신하기 위해 사용한다.
// CheckBox Init.
chkRefresh.Text = "Refresh";
chkRefresh.Checked = false;
this.Controls.AddRange(new System.Windows.Forms.Control[] {
lstProcess,
lblTotal,
panTotal,
chkRefresh
});
} // end of MyForm()
#endregion
위 코드의 마지막에는 폼에 Load 이벤트를 추가하고, 이벤트에 사용할 함수를 위임했다. 다음은 ListView 를
80
private void InitializelstProcess()
lstProcess.Clear();
colHeader1,
colHeader2,
colHeader3,
colHeader4,
colHeader5,
colHeader6
});
if ( chkRefresh.Checked )
RefreshList();
InitializelstProcess();
81
FillProcessView();
try
m_threadCount = 0;
procs = Process.GetProcesses();
m_processCount = procs.Length;
lstProcess.BeginUpdate();
// Physical/Virtual Mem in KB
strThread[0] = aProc.ProcessName.ToString();
strThread[1] = aProc.Id.ToString();
strThread[2] = aProc.BasePriority.ToString();
82
strThread[5] = aProc.StartTime.ToString();
lstProcess.Items.Add(item);
lstProcess.EndUpdate();
catch(Exception e)
evlog.Log = "Application";
evlog.Source = e.Source;
evlog.WriteEntry(e.Message);
83
private void lstProcess_OnClick(object sender, System.EventArgs e)
ListViewItem li = lstProcess.SelectedItems[0];
aFrm.DisplayForm(currProcID, currProcName);
if(components != null)
components.Dispose();
base.Dispose(disposing);
84
마지막은 자원을 정리하는 부분이다. 여기서는 굳이 정의하지 않아도 되지만 예의상(?) 사용하였다. Dispose 는
코드를 작성했으면 다음은 쓰레드 정보를 보여주는 WinTopThreadInfo 클래스를 작성하도록 하자.
이름 : WinTopThreadInfo.cs
using System;
using System.Windows.Forms;
using System.Drawing;
using System.ComponentModel;
using System.Diagnostics;
using System.Collections;
namespace WinTop
85
System.ComponentModel.Container components 와 같이 사용하여, 폼에서 사용하는 컴포넌트에 대한
컨테이너로 사용한다.
public WinTopThreadInfo()
// Label Init.
// ListView Init.
lstThread.Name = "lstThread";
lstThread.FullRowSelect = false;
lstThread.View = View.Details;
lstThread.Sorting = SortOrder.Ascending;
InitializeThreadList();
this.Controls.AddRange(new System.Windows.Forms.Control[] {
lblTotalThread,
lstThread
});
86
} // end of WinTopThreadInfo
} // end of InitializeThreadList
m_Threads = GetThreads(procID);
ProcessThreadCollection threads;
Process proc;
proc = Process.GetProcessById(procID);
threads = proc.Threads;
m_threadCount = threads.Count;
87
for(int Indx = 0; Indx < threads.Count; Indx++)
strThreads[0] = threads[Indx].Id.ToString();
strThreads[1] = threads[Indx].CurrentPriority.ToString();
strThreads[2] = threads[Indx].BasePriority.ToString();
strThreads[3] = threads[Indx].TotalProcessorTime.ToString();
strThreads[4] = threads[Indx].ThreadState.ToString();
strThreads[5] = threads[Indx].StartTime.ToString();
strThreads[6] = threads[Indx].UserProcessorTime.ToString();
lstThread.Items.Add(li);
} // end of FillThreadList
try
Process proc=Process.GetProcessById(procID);
return threads;
catch ( Exception )
return null;
88
} // end of GetThreads
프로세스 ID 를 이용하여 프로세스에 속한 쓰레드를 가져온다. 가져오는 데 실패하면 null 을 반환하도록 한다.
if(components != null)
components.Dispose();
base.Dispose(disposing);
} // end of Dispose
원하는 프로세스를 종료할 수도 있다. 시스템의 페이지된 메모리 크기와 페이지 되지 않은 메모리 크기를 알고
들어있는 MSDN 은 닷넷을 위하여 나온 것으로 MSDN Subscription 으로 제공되는 MSDN 과 MS 의 MSDN
적절한 관련 자료에 대한 URL 을 제공하지 못하는 것을 안타깝게 생각하지만 MSDN.NET 에서 충분한 자료를
찾아볼 수 있을 것이다 생각한다. VS.NET beta 2 에서는 ListView 를 추가하고 컴파일하면 폼에서 생성하는
89
코드에 오류가 있어서 실행되지 않을 것이다. (VS.NET RC 에서는 잘 동작한다.) 따라서 윈도우 폼에 대해서 잘
쓰레드와는 무관하지만 시스템의 정보를 알아내려면 다음과 같은 코드를 컴파일하여 실행해보기 바란다.
이름 : info.cs
namespace csharp
using System;
using System.Collections;
si.Display();
90
Console.WriteLine("System Directory : {0}", Environment.SystemDirectory);
int Indx = 0;
Indx++;
이상으로 시스템에 있는 프로세스와 쓰레드 정보를 알아내고, 제어할 수 있는 방법에 대해서 알아보았다. 보다
자세한 부분에 관심이 있는 분들은 MSDN 등의 온라인 문서를 참고하기 바란다. 마지막으로 어찌보면 참으로
쓸데없는 내용을 적어놓은 건지도 모르지만, 과연 이러한 것들을 어디에 써먹을 것인가를 한 번은 나름대로
예제를 위해서 비교적 간단하게 작성했기 때문에 다소 객체 지향과는 동떨어진 구성을 했지만, 전체적인 틀을
새롭게 작성해서 리모팅이나 웹 서비스를 이용해서 원격으로 시스템의 프로세스 정보를 얻을 수도 있을 것이고,
91
것이다. 추가적으로 NetWkstaGetInfo()와 같은 Win32 API를 이용한다면 원격으로 특성 서버의 종류(PDC, BDC,
SQL Server, Novell Network Server, NT Member, Windows 9x)를 알아내는 것도 가능할 것이다. 기존에 작성한
클래스를 웹 서비스로 제공하는 wsdl.exe은 /username, /password, /domain과 같은 옵션을 이용하여 관리자
권한으로 접근하도록 제한된 영역에 접근할 수 있는 프록시 클래스를 생성할 수 있으며, 관리자 권한으로 할 수
기다리던 멀티 쓰레드에서의 동기화에 대해서 다룰 것이다. 늘 하는 얘기지만 필자는 전문가도 아니며 모르기
벌이는 것을 좋아한다. 필자가 틀렸다고 뒤에서 험담하기 보다는 For Guru나 email을 통해서 필자에게 틀린
접한지 16 년째가 되고, 프로그래머로 일한지 6 년이 되어 간다니 새삼스럽다. C#이라는 유행과 같은 언어로
쓰레드에 쓰고 있지만, 그 보다는 그 기본이 되는 바탕을 설명하려고 하는데 그러지 못해 아쉬운 점이 많다.
갈수록 느끼고 있다. 끝으로 필자의 부족한 실력이나마 WinTop을 작성할 수 있도록 도와준 프로그래머들에게
감사드린다. 전체 뼈대를 제공한 Melanie, ListView와 관련하여 조언을 아끼지 않았으며 지금까지의 C# 쓰레드
글들에 대해서 끊임없는 피드백을 해주신 4baf, Linux에서의 쓰레드로 필자를 괴롭히는 dicajohn, Perl에서의
쓰레드 프로그래밍에 대한 조언과 도움을 준 nuthack, 원고를 쓸 때마다 MSN으로 괴롭히는 운령, 모르는
아니다. -_-)
92
• 『VB.NET In a Nutshell』(Steven Roman, Ron Petrusha, Paul Lomax 저, 오라일리, 2001) - 쓰레드
도움이 되고 있다.
• 『Subclassing & Hooking with Visual Basic』(Stephen Teilhet 저, 오라일리, 2001) - 비록 비주얼
• 『C# & .NET Platform』- C#과 닷넷 플랫폼에 대한 자세한 해설이 들어있지만 솔직한 느낌은 두께에
참고가 되었다.
93
C# 쓰레드 이야기: 8. 동기화
이번에는 쓰레드 동기화에 대해서 알아보도록 하자. 쓰레드 프로그래밍은 상당히 어렵다. 쓰레드 프로그래밍이
어려운 이유는 쓰레드에 할당되는 CPU 시간을 프로그래머가 제어할 수 없기 때문이다. 전에 얼핏 얘기한
실행되는지 프로그래머가 알 수 없다. 실은 이것보다 더 중요한 문제가 있는데 하나의 CPU 를 사용하고 있는
프로그래밍을 경험하는 것이 다르다는 것이다. 1 개의 CPU 에서 잘 동작하는 코드가 2 개의 CPU 에서는 제대로
독자들은 하나의 CPU 를 사용하고 있는 환경이기 때문에 이것이 올바른 멀티 쓰레드 코드인지 알 수 없다. -
동기화에 대해서 또 하나의 중요한 주제인 프로세스 우선 순위와 쓰레드 우선 순위에 대해서는 이전 글에서
다루었으며, 멀티 프로세서 환경에서 쓰레드가 실행될 프로세스를 지정하는 방법에 대해서, 즉 프로세서
최소 단위 오퍼레이션
단위를 말한다. 각각의 쓰레드에게 CPU 시간이라는 '밥'을 주는 인터럽트는 하드웨어에 의해서 비동기적으로
생각해보자. A 가 100 만원을 갖고 있는데 10 만원을 B 계좌로 옮기려고 한다. 다음은 예금을 옮기려고 하는
A 의 계좌에 10 만원이 있는지 확인하는 순간에 인터럽트가 발생하여 a 쓰레드는 중지하고, c 쓰레드가
94
실행된다. c 쓰레드는 C 의 계좌에서 40 만원을 A 의 계좌로 옮겼으며, A 의 잔액은 140 만원이 되었다. 이
알고 있으므로 10 만원을 뺀 90 만원으로 변경했다. A 계좌에 있는 잔액은 실제로 90 만원이 아니라 130 만원이
되어야한다.(100 - 10 + 40 만원)
if ( i == 53 )
Console.WriteLine(i.ToString());
다음과 같다.
IL_0004: ldc.i4.s 53
이것은 IL 코드이며, 해당 CPU 에 맞는 기계어로 변환된다면 코드는 더 길어질 것이며, 이 실행의 중간에 다른
이러한 종류의 버그는 가끔 일어나며, 재현하기 어렵기 때문에 디버깅하는 것도 어렵다. 위에서 예로든 은행
커널 객체
95
커널 오브젝트는 운영체제의 자원을 뜻하며, 주로 쓰레드, 프로세스, 이벤트, 뮤텍스, 세마포어, 파일, 파일 매핑,
부분은 Win32 와 깊게 관련된 부분이고, 닷넷과는 관련이 없어보이지만 나중에 Win32 와 닷넷이 얼마나 비슷한
경쟁 조건(race condition)
생산자/소비자 프로그래밍 모델은 하나의 쓰레드는 데이터를 생성하도록 하고, 다른 쓰레드는 데이터를
소비하도록 하는 것이다. 이 모델의 이점은 생산자가 최대한의 데이터를 생성하도록 하고, 소비자는 최대한
예를 들어서, 생산자가 정수를 생성하고 소비자는 생성된 정수를 소비한다고 하자. 생산자가 정수를 생산하는
속도가 소비자가 정수를 소비하는 속도보다 빠르다면 소비자는 생산자가 생성한 숫자들을 속도차 만큼 놓치게
된다.
반대로 생산자가 정수를 생산하는 속도보다 소비자가 정수를 소비하는 속도가 더 빠르다면 이미 가져간 값을
96
이와 같이 여러 개의 쓰레드가 동시에 같은 자원을 공유하고 있을 때 발생하게 되며, 동기화를 사용하여 이
B 의 쓰레드 우선 순위가 A 보다 상대적으로 낮다고 하자. A 가 CPU 할당 시간에 작업을 끝내고 대기 상태로
전환되고, 쓰레드 B 도 대기 상태라고 하자. 그러면 우선 순위 정책에 의해서 A 가 다시 CPU 시간을 할당받게
된다. 만약 적절한 정책이 사용되지 않는다면 쓰레드 A 가 종료될 때까지 쓰레드 B 는 CPU 시간을 할당 받을
교착 상태(deadlock)
자원을 대기하는 경우를 뜻한다. 예를 들어서 A 쓰레드가 b 자원을 소유하고 있고, B 쓰레드가 a 자원을
97
소유하고 있다. A 쓰레드는 실행을 위해서 a 자원을 획득하기 위해 대기하고 있으며, B 쓰레드는 실행을 위해서
b 자원을 획득하기 위해 대기하고 있다고 하자. 이 경우에 각각의 쓰레드는 소유하고 있는 자원을 해제하기
다르다. 기존의 멀티 쓰레드 프로그래밍에서는 자원의 해제에 대해서 프로그래머가 신경써야 했으나,
그러나 쓰레드 내에서 생성한 객체가 비관리형 코드(unmanaged code)로 작성된 라이브러리라면 쓰레드를
가비지 컬렉터 : 두 가지 버전
서버 버전(mscorsvr.dll)이다.
서버 버전 GC
• 다중 프로세서 지원, 병렬 처리 지원
98
• CPU 마다 하나의 GC 쓰레드
웍스테이션 GC
당연한 얘기겠지만 가비지 컬렉터로 인해서 자원 정리에 대해서 신경쓰지 않아도 된다는 이점이 있지만 서버
버전의 GC, 즉 멀티 프로세서 환경에서의 자원 정리에 대해서는 객체의 부활과 관련한 문제가 발생한다.
마치며
진일군에게 감사를… -
프로세스나 쓰레드에 대해서는 많은 내용들이 있고, 더 자세한 내용을 알고 싶다면 Operating System Concepts,
몇 가지 용어와 개념에 대해서 간단히 정리하는 것으로 마친다. 동기화에는 임계 영역, 세마포어, 뮤텍스등
참고
http://www.brpreiss.com/books/opus6/html/page417.html
Bruno R. Preiss 의 홈페이지이며, Data Structures and Algorithms with Object-Oriented Design
99
대해 설명하고 있다. 자바와 닷넷 모두 자원 정리를 위해 Mark & Sweep 알고리즘을 사용하고 있으며,
http://www.programmersheaven.com/zone28/articles/article667.htm
소스 다운로드(resurrection.cs)
마지막으로 이 소스는 필자가 고의로 부활한 객체를 가비지 컬렉터가 제거하지 못하도록 작성한
100
C# 쓰레드 이야기: 9. 임계 영역
지난 시간에는 동기화에 대해서 이야기 했으며, 멀티 쓰레드 환경에서 어떤 문제가 생길 수 있는지 간략히
감소를 동기화할 수 있는 Interlocked 클래스가 제공된다. 이것은 동기화의 가장 간단한 형식에 속한다. 그러나
정수형이 아닌 데이터베이스에 데이터를 쓰거나 읽는 작업을 쓰레드를 이용해서 동시에 처리한다면 데이터를
것을 베타적 접근이라한다.
임계 영역(Critical Section)
영역(Section)을 뜻한다. 임계 영역의 구조는 보통 운영체제에 의해서 숨겨져 있으며, 닷넷에서도 이점은
임계 영역은 프로세스 공간에 있으며, 다른 프로세스와 직접 통신할 수 없다. 닷넷에서는 이러한 프로세스는
커널 객체에 대해서 소개한 것을 기억하고 있다면 알 수 있겠지만, 임계 영역은 커널 객체가 아니다. 때문에
각각의 프로세스마다 고유의 임계 영역을 갖고 있으며, 다른 프로세스에 의해서 임계 영역을 침범할 수 없다.
101
닷넷에서 동기화 객체
닷넷에서는 동기화를 위한 몇가지 객체가 제공된다. 다음은 닷넷의 주요 동기화 객체의 클래스 계층이다.
System.Object
System.MarshalByRefObject
System.Threading.WaitHandle
System.Threading.AutoResetEvent
System.Threading.ManualResetEvent
System.Threading.Mutex
ManualResetEvent, Mutex 클래스가 있으며, 이들 각각의 동기화 객체는 실행시간에 WaitHandle 객체로
대표해서 처리할 수 있다는 것을 알 수 있다. 또한, WaitHandle 클래스는 Win32 동기화 핸들을 캡슐화하고
생산자/소비자 프로그래밍 모델에 대해서 소개했는데 닷넷에서는 이것에 대해서 ReaderWriterLock 으로 구현해
Win32 에서의 임계 영역
때문이다. Win32 에 대한 코드는 모두 C++를 사용했다. Win32 에서의 임계 영역은 다음과 같은 순서로 된다.
Win32 임계 영역 의사 코드
102
// 임계 영역을 선언한다.
CRITICAL_SECTION cs;
// 임계 영역을 초기화한다.
InitializeCriticalSection(&cs);
EnterCriticalSection(&cs);
try
finally
닷넷에서의 임계 영역 - Monitor
이름 : CritSec01.cs
103
using System;
using System.Threading;
app.DoTest();
t1.Start();
t2.Start();
Monitor.Enter(this);
104
try
counter++;
} // end of try
finally
Monitor.Exit(this);
} // end of finally
} // end of Incrementer
Monitor.Enter(this);
try
counter--;
} // end of try
105
finally
Monitor.Exit(this);
} // end of finally
} // end of Decrementer
변경하는 코드 부분을 Monitor 클래스를 사용하여 임계 영역으로 선언하고 있다. 코드를 컴파일하고 실행하면
106
Monitor.TryEnter()는 Monitor.Enter()와 비슷하지만 임계 영역이 사용중이더라도 기다리지 않고 계속해서
위 예제에서 사용한
Monitor.Enter(this);
를 모두 다음과 같이 변경한다.
Monitor.TryEnter(this);
Decrementer()만을 수정했다.
107
private void Decrementer()
if ( Monitor.TryEnter(this) == false )
// 임계 영역에 들어간 경우
else
counter--;
Monitor.Exit(this);
} // end of Decrementer
닷넷에서의 임계 영역 - lock()
다음은 예제에서 사용했던 Monitor 클래스 대신에 lock()을 사용하여 동기화를 수행하도록 변경한 것이다.
lock(this)
108
{
counter++;
} // end of lock
} // end of Incrementer
lock(this)
counter--;
} // end of lock
} // end of Decrementer
자원에 대해서 정교한 제어가 필요할 때 사용한다. 그렇다면 이러한 Monitor 클래스와 lock()은 어떻게 구현된
Win32 에서 Monitor 구현
닷넷에서 사용한 Monitor 클래스를 Win32 에서 구현해보도록 하자. 먼저 동기화에 대한 공통된 인터페이스를
109
이름 : SyncHandle.h
#include <windows.h>
class SyncHandle
protected:
public:
SyncHandle::SyncHandle();
SyncHandle::~SyncHandle();
};
hObject 는 객체에 대한 핸들을 갖고 있으며, 생성자와 파괴자를 정의하고, 임계 영역에 들어가는 enter 와
exit 를 구현하고 있다. 여기서는 간단히 하기 위해 enter 에 대해서는 오버로딩을 구현하지 않았다. 독자중에
이름 : SyncHandle.cpp
#include "SyncHandle.h"
SyncHandle::SyncHandle()
110
hObject = NULL;
SyncHandle::~SyncHandle()
if ( NULL != hObject )
::CloseHandle(hObject);
hObject = NULL;
return true;
else
return false;
파괴자에서는 객체에 대한 핸들이 있는지 확인하며, 핸들을 갖고 있다면 핸들을 정리한다. enter 는 실제로 임계
영역에 들어가는 것이다. enter 의 실제 구현은 WaitForSingleObject 로 되어 있는데 하나의 객체에 대해서만
임계 영역에 들어갈 때까지 기다린다는 것을 의미한다. enter 의 선언부분을 보면 virtual bool enter(DWORD
WAIT_FAILED 를 사용한다)
이름 : Monitor.h
111
#include <windows.h>
#include "SyncHandle.h"
CRITICAL_SECTION cs;
public:
Monitor();
~Monitor();
bool tryEnter();
};
이름 : Monitor.cpp
#include "Monitor.h"
Monitor::Monitor()
// 임계 영역을 초기화한다
InitializeCriticalSection(&cs);
112
Monitor::~Monitor()
// 임계 영역을 정리한다
DeleteCriticalSection(&cs);
// 임계 영역에 들어간다.
EnterCriticalSection(&cs);
return true;
bool Monitor::exit()
// 임계 영역에서 빠져나온다
LeaveCriticalSection(&cs);
return true;
bool Monitor::tryEnter()
if ( TryEnterCriticalSection(&cs) )
return true;
else
#endif
113
return false;
Monitor 클래스는 .NET 의 Monitor 클래스와 큰 차이를 느끼지 못할 것이다. .NET 에서 사용했던
않도록 하려면 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 <windows.h>
#include <iostream>
#include "Monitor.h"
// 쓰레드 함수의 원형
Monitor monitor;
int counter = 0;
114
int main()
DWORD threadID;
HANDLE hThreads[nThread];
0,
incrementer,
(LPVOID)ps[0],
0,
&threadID);
0,
decrementer,
(LPVOID)ps[1],
0,
&threadID);
//DWORD rc =
115
CloseHandle(hThreads[0]);
CloseHandle(hThreads[1]);
return 0;
monitor.enter();
char* ps = reinterpret_cast<char*>(pv);
counter++;
monitor.exit();
return 0;
monitor.enter();
char* ps = reinterpret_cast<char*>(pv);
116
{
counter--;
monitor.exit();
return 0;
입력된 코드의 컴파일은 다음과 같이 한다. 참고로 모든 C++ 코드는 순수 C++로 작성되었으며 Borland C++나
Visual C++ 양쪽 모두에서 제대로 컴파일된다.(필자는 C++ Builder 5.0 과 Visual C++ 6.0 으로 테스트하였다)
bcc32 -c SyncHandle.cpp
bcc32 -c Monitor.cpp
cl /c SyncHandle.cpp
cl /c Monitor.cpp
뜻한다. VC++은 몇가지 차이점이 있기 때문에 예외가 발생하면서 컴파일되기 때문에 예외를 화면에 출력하지
117
결과에서 알 수 있는 것처럼 닷넷에서 했던것과 Win32 API 를 사용하여 직접 Monitor 클래스를 구현한 것과 큰
차이가 없다는 것을 알 수 있을 것이다. 여기서는 일부러 Win32 API 를 사용하여 Monitor 클래스를 직접
구현했다. 임계 영역이 무엇인지, Monitor 클래스가 어떻게 동작하는지 알고 있다면 문제가 생겼을 때 문제의
원인을 하나씩 풀어가기 쉬운 것은 당연할 것이다. 마지막으로 글이 길어지면 한빛미디어 편집진에게 혼나기
때문에 닷넷의 lock()에 대한 구현은 여기서 소개하지 않고, 소스에만 포함시켜 두었다. 관심있는 분들은 Lock.h,
Lock.cpp, CritSec02.cpp 를 살펴보기 바란다. C#에서의 lock()을 C++에서는 Lock l(&cs);와 같이 사용한다는
정도의 차이만이 있을 뿐이다.(Lock 클래스는 Lock.h 에 정의되어 있다) 이 소스를 곰곰히 분석해보면 닷넷에서
Win32 API 구현에서 보면 WaitForSingleObject Win32 API 를 볼 수 있는데 이것은 세번째 글에서 다루었던
118
닷넷에서는 이 부분이 크게 세가지로 나누어져 있다: WaitOne, WaitAny, WaitAll.
마치며
뮤텍스(Mutex)에 대해서 설명할 것이다. 뮤텍스는 매우 강력한 동기화 도구지만 커널 객체이기 때문에 성능이
떨어지지만 프로세스간에 동기화를 수행할 수 있다. 왜 뮤텍스를 써야하는지 다음 시간에 자세히 살펴보도록
하자. 마지막으로 멋진 동기화 코드를 소개한 Julian 에게 감사드리며, Win32 와 닷넷간의 차이점에 대해서
119
C# 쓰레드 이야기: 10. 뮤텍스(Mutex)
lock()에 대해서 살펴보았다. 이번에는 동기화에 유용하게 사용할 수 있는 뮤텍스에 대해서 살펴보도록 하자.
신호 메커니즘(Signaling Mechanism)
객체를 동기화 객체라 한다. 이러한 동기화 객체는 사용할 수 있는 상태를 알려줄 수 있다. 이러한 상태를
사용한다.
Signaled - 대부분의 도서들이 Signaled 원문으로 표시하거나 시그널 되었다로 표시하고 있으나 이러한
쓰레드에 대한 설명에서 빠지지 않는 고전적인 문제가 있으니, 그것을 식사하는 철학자 문제라 한다. 이 문제는
다음과 같다. 하나의 테이블에 다섯명의 철학자가 앉아있다. 테이블에는 5 개의 스파게티 접시가 있고, 5 개의
포크가 있다. 스파게티는 매우 미끄럽기 때문에 식사를 하려면 2 개의 포크를 사용해야한다. 철학자는 생각하는
120
것과 식시하는 것, 두 가지 일만 할 수 있다. 철학자는 생각을 하다가 배가 고프면 식사를 한다.
이 문제에서 철학자가 쓰레드를 뜻하는 것임은 잘 알 수 있을 것이다. 철학자가 식사를 하려고 할 때 이용할 수
있는 포크가 있으면 먼저 오른쪽 포크를 든다. 그 다음에 왼쪽 포크를 이용할 수 있는지 확인하고 남아있는
포크가 있으면 왼쪽 포크를 집어서 식사를 한다. 루프를 돌면서 다섯 개의 포크를 이용할 수 있는지 확인하는
것도 하나의 방법이 될 것이다. 그러나 이 경우에 교착상태(deadlock)에 빠질 위험이 있다. 만약에 다섯명의
철학자가 동시에 배가 고파져서 동시에 오른쪽 포크를 집는 다면 어떻게 될까? 모든 철학자가 다른 포크를
이용할 수 있을 때까지 기다리게 되므로 철학자는 굶어죽게 된다. 이런 경우에는 하나의 포크를 집은 다음에
철학자가 포크를 내려놓는 순간 다른 철학자가 포크를 들어서 식사한다고 생각해 보자. 세 명의 철학자는 한
• 홀수번째 철학자는 오른쪽 포크를 먼저들고, 짝수번째 철학자는 왼쪽 포크를 먼저들도록 한다.
여기서 마지막 해결 방법이 획기적인 아이디어로 생각되지만, 이 세 가지 해결책은 하나의 철학자가 굶어죽는
121
철학자가 생각을 하다가 배가 고프면 양쪽에 포크를 이용할 수 있는지 확인한다. 양쪽에 있는 포크를 모두
이용할 수 있으면 식사를 하지만, 그렇지 않다면 식사를 하지 않고 대기중(wait)으로 표시한다. 양쪽에 있는
포크를 모두 이용할 수 있으면 식사를 시작하고 식사중으로 표시한다. 식사가 끝나면 포크를 다시 양옆에
내려놓고 식사가 끝났다는 사실을 대기중인 철학자에게 알려준다(signaling). 여기서 대기중인 철학자는
누군가가 알려주기 전에는 절대 깨어나지 않는다. 그러나 대기중인 철학자가 아무도 없다면 식사가 끝났다는
멀티 쓰레드 프로그래밍에는 자주 발생하기 문제에 대한 몇 가지 동기화 방법이 있으며, 이러한 동기화 방법을
설계 패턴(Design Pattern)이라 한다. 식사하는 철학자 문제에 사용하는 방법은 상호배제(Mutual Exclusion)이며,
간단히 뮤텍스(Mutex)라 한다. 뮤텍스는 공유 자원에 대해서 상호 배타적 접근을 위해서 사용된다.
뮤텍스(Mutex)
하나다. 커널 객체는 보안과 관련되어 있기 때문에 자원을 사용하기 전에 보안을 확인하기 때문에 임계 영역을
사용하는 것보다 느리다. Win32 에서 뮤텍스는 Win32 커널 객체에 속하며, 닷넷에서는 닷넷 커널 객체에
방법을 제공한다. 뮤텍스는 커널 객체이기 때문에 프로세스간에 통신을 하기 위해서 사용할 수 있다.
Mutex 클래스
생성자 설명
122
public Mutex(); 기본값으로 뮤텍스를 초기화한다.
첫번째는 기본값으로 뮤텍스를 초기화하는 것이다. 두번째는 뮤텍스를 생성하는 쓰레드가 뮤텍스의 소유권을
갖도록 할 것인가를 결정한다. true 로 되면 뮤텍스를 생성한 쓰레드가 소유권을 갖게된다. 뮤텍스를 이용하여
프로세스간에 동기화를 하려면 뮤텍스 이름을 이용한다. 세번째와 네번째 생성자는 뮤텍스의 이름을 지정한다.
myMutex 와 같은 문자열을 이름으로 사용하여 동기화할 수 있으며, 각각의 쓰레드는 같은 뮤텍스에 대해서
식사하는 철학자 문제에서와 같이 공유 자원을 뮤텍스가 관리하도록 한다. 공유 자원이 필요한 스레드는
mutex.WaitOne();
// 공유 자원 사용
mutex.ReleaseMutex();
WaitOne()은 하나의 자원이 이용가능해질 때까지 대기하는 것을 뜻한다. 이 메소드는 WaitHandle 클래스에서
상속받은 것이다. WaitOne(), WaitAny(), WaitAll()을 사용할 수 있다. 뮤텍스에서는 Wait 를 호출한 만큼
ReleaseMutex() 호출해야한다. 그렇지 않으면 자원은 해제되지 않으며 다른 쓰레드들은 계속해서 자원이
123
Monitor 나 Mutex 를 사용하여 공유 자원에 대한 동기화를 할 때 다음과 같은 코드를 작성하지 않도록 주의한다.
get
mMutex.WaitOne();
return lData;
mMutex.ReleaseMutex();
빨간색으로 표시된 부분과같이 공유 자원에 대한 락을 획득한 상태에서 함수를 빠져나가면 자원은 해제되지
않고 교착상태에 빠지게된다. 따라서 Mutex 를 먼저 해제한 다음에 return 을 사용하여 값을 반환하고 함수를
빠져나가도록 해야한다. 다행히도 닷넷에서 이러한 종류의 오류를 하게되면 코드는 컴파일되지 않으며
이름 : Mutex01.cs
using System;
using System.Threading;
public SharedDataObject ()
124
}
get
mMutex.WaitOne();
Thread.Sleep(100);
mMutex.ReleaseMutex();
return lData;
set
mMutex.WaitOne();
Thread.Sleep(2000);
mData = value;
mMutex.ReleaseMutex();
125
public class AppMain
AppMain()
Console.WriteLine("Begin Application");
~AppMain()
Console.WriteLine("End Application");
ap.DoTest();
msdo.Data = 0;
126
Thread[] threads = {
};
threads[0].Start();
threads[1].Start();
threads[2].Start();
threads[3].Start();
Thread.Sleep(3000);
127
}
Console.WriteLine("Started - DoWriting1()");
msdo.Data++;
Console.WriteLine("Ended - DoWriting1()");
get 과 set 에서 콘솔에 메시지를 출력하도록 하였다. get 과 set 부분을 자세히 살펴보도록 하자.
get
mMutex.WaitOne();
Thread.Sleep(100);
mMutex.ReleaseMutex();
128
return lData;
set
mMutex.WaitOne();
Thread.Sleep(2000);
mData = value;
mMutex.ReleaseMutex();
있을까?
129
예제와 같은 것을 단일 생산자/다중 소비자 모델이라고 한다. 닷넷에서는 이러한 모델에 대한 클래스를
lock()의 함정
마지막으로 지난 시간에 간단하게 다루었던 lock()에 대한 것인데 다음과 같이 사용하면 절대로 동기화되지
않는다.
int iData = 0;
lock(iData)
// do something
결과적으로 iData 값은 새로운 스택에 복사된다. 때문에 위와 같은 메소드가 호출될 때마다 스택에 이 정수형의
되기 때문에 어떤 쓰레드도 블록되지 않는다. 따라서 lock()에는 레퍼런스 타입만 사용할 수 있다.(일반적으로
System.Type 클래스의 객체는 클래스의 static 메소드를 사용하여 상호 배제를 구현할 수 있다.
class Shared
130
public static void Add(object x) {
lock (typeof(Shared)) {
// do something
lock (typeof(Shared)) {
// do something
마치며
이번에는 뮤텍스에 대해서 알아봤다. 아직 뮤텍스를 제대로 설명한 것은 아니다. 다음 시간에는 뮤텍스에
대해서 보다 자세히 알아볼 시간을 가질 것이다. WaitOne, WaitAny, WaitAll 의 사용에 대해서 이해하는 시간을
가질 것이며, 이벤트에 대해서도 살펴볼 것이다. 아마도 몇 주 후에는 지금까지 살펴본 클래스들에 대해서
131
C# 쓰레드 이야기: 11. 이벤트(Event)
지난 시간에는 식사하는 철학자 문제와 뮤텍스에 대해서 소개했으며, 이번에는 뮤텍스의 나머지 부분에 대해서
이벤트 지금까지 살펴본 모니터나 뮤텍스는 하나의 쓰레드가 공유 데이터를 액세스하는데 유용하지만, 여러
개의 쓰레드가 서로의 실행을 방해하지 않으면서 쓰레드간에 메시지를 주고 받으려면 어떻게 해야할까?
이런 경우를 생각해보자. A 쓰레드가 작업이 끝나면 이 사실을 전달받은 B 쓰레드가 작업을 시작한다. 두
쓰레드간에 데이터를 공유할 필요도 없고, 동기화를 사용할 필요도 없다. 이와 같은 작업을 파이프 라인으로
참고 도서
IT 백두대간, C# 프로그래밍
김대희
목차보기
132
각각의 이벤트는 실제로 닷넷에서 AutoResetEvent 와 ManualResetEvent 클래스로 구현되어 있다. 이들
initialState 는 true 나 false 를 사용할 수 있으며, 이벤트의 초기상태를 신호상태로 할 것인지, 비신호상태로 할
신호가 될 때까지 신호등 앞에서 기다린다는 의미로 해석하면 된다. 즉, 처음부터 이벤트는 대기상태가 된다.
필자는 쓰레드들의 각각의 작업이 Step1 → Step2 → Step3 으로 수행되기를 원한다. 또한 이들 각각의 작업은
데이터를 공유하지 않으며, 단순히 대기중인 쓰레드에게 작업이 끝났다는 사실만을 알려주기 위해 이벤트를
Console.WriteLine("Processing Step1");
Thread.Sleep(3000);
areStep1.Set();
먼저 화면에 Step1 을 처리중이라는 메시지를 출력한다. 그리고 실제로 어떤 작업을 수행하는 함수가
사용하여 3 초간 처리중인 것처럼 하였다. 처리가 끝나면 areStep1.Set()을 사용하여 첫번째 이벤트 areStep1 을
133
신호상태로 변경한다. areStep1 이 신호상태로 바뀐 것을 감지하고 작업을 수행하는 쓰레드는 Step2 이다. 이제
Step2 는 어떻게 Step1 의 쓰레드가 이벤트를 신호상태로 바꾼 것을 알고, 작업을 처리하는지 살펴보자.
areStep1.WaitOne();
Console.WriteLine("Processing Step2");
Thread.Sleep(1000);
areStep2.Set();
그것을 감지하고 대기중인 쓰레드를 잠에서 깨우는 역할을 하는 부분이다. Step2 에서도 Thread.Sleep(1000)
이름 : event01.cs
using System;
using System.Threading;
134
public void Step1()
// do something
Console.WriteLine("Processing Step1");
Thread.Sleep(3000);
areStep1.Set();
areStep1.WaitOne();
Console.WriteLine("Processing Step2");
Thread.Sleep(1000);
areStep2.Set();
areStep2.WaitOne();
Console.WriteLine("Processing Step3");
areStep3.Set();
135
Thread thread2 = new Thread(new ThreadStart(Step2) );
thread1.Start();
thread2.Start();
thread3.Start();
ap.DoTest();
있는 것처럼 이벤트의 상태가 자동으로 초기화되느냐 그렇지 않느냐의 차이다. 위의 예제에서 Step2 를
처리하는 쓰레드는 이벤트가 신호상태가 되기를 대기한다. 신호상태가 되면 이벤트는 쓰레드를 통과시키고 다시
자동으로 비신호상태가 된다. 따라서 직접 Reset()을 사용할 필요가 없다. 반면에 ManualResetEvent 는 한 번
ap.DoTest();
ap2.DoTest();
136
}
ap.DoTest();
ap.DoTest();
같은 단계별 작업이 필요하다면 최소한 3 개의 쓰레드가 이벤트를 통해서 신호를 주고 받으면서 작업할 수 있을
mreStep1.WaitOne();
mreStep1.Reset();
Console.WriteLine("Processing Step1");
Thread.Sleep(3000);
137
mreStep1.Set();
하나의 대기 쓰레드를 통과시킨 다음에 자동으로 비신호상태가 되지 않으므로 Reset()을 호출하여 명시적으로
비신호상태로 전환한다.
이름: event02.cs
using System;
using System.Threading;
mreStep1.WaitOne();
mreStep1.Reset();
Console.WriteLine("Processing Step1");
Thread.Sleep(3000);
mreStep1.Set();
mreStep1.WaitOne();
138
mreStep1.Reset();
Console.WriteLine("Processing Step2");
Thread.Sleep(1000);
mreStep2.Set();
mreStep2.WaitOne();
mreStep2.Reset();
Console.WriteLine("Processing Step3");
mreStep3.Set();
thread1.Start();
thread2.Start();
thread3.Start();
139
public static void Main()
ap.DoTest();
다음과 같이 수정한다.
thread1.Start();
thread2.Start();
thread3.Start();
mreStep1.Set();
140
쓰레드가 하나의 작업을 처리하기 위해서 메시지를 주고 받는데 유용하며, ManualResetEvent 는 여러 개의
쓰레드가 동시에 여러 개의 작업을 처리하기 위해서 메시지를 주고 받는데 유용하다. 예를 들어서, 사용자가
워드 파일을 읽어들였을 때, 문서에 있는 단어수를 세는 쓰레드가 하나, 문서를 화면에 표시하는 쓰레드가 하나,
맞춤법을 검사하는 쓰레드가 하나, 인쇄를 하는 쓰레드가 하나. 이렇게 4 개의 쓰레드가 문서를 읽어들이는
필요하다면 자신이 만들어쓰도록 한다. 이러한 Pulse()는 Monitor 클래스에서 볼 수 있다.(Pulse 나 PulseAll 에
마치며
뮤텍스와 이벤트가 무슨 관계가 있을까?라고 생각하는 분들도 있을 것이다. 이벤트가 뮤텍스의 소유권에 대한
생각할 수 있을 것이다. 다음에는 이벤트와 뮤텍스를 이용하는 것에 대해서 살펴보도록 하자. 끝으로 다음에는
141
C# 쓰레드 이야기 - 12. 식사하는 철학자
설명했다. Mutex 를 사용하여 자원을 동기화하는 방법을 살펴봤으며, 이벤트를 사용하여 쓰레드간에 신호를
WaitHandle 클래스
Mutex를 이용한 동기화와 식사하는 철학자에 대해서 설명하기 전에 WaitHandle 클래스부터 설명하는 것으로
시작하자. 9. 임계 영역에서 동기화 클래스 계층구조를 보여준 적이 있다. 기억이 안나는 분들을 위해서 다시
System.Object
System.MarshalByRefObject
System.Threading.WaitHandle
System.Threading.AutoResetEvent
System.Threading.ManualResetEvent
System.Threading.Mutex
동기화를 제공하는 Win32 API 에 대한 래퍼 클래스다. WaitHandle 클래스는 추상 클래스이므로 동기화 객체를
142
MsgForMultipleObjects, WaitForInputIdle, WaitForDebugEvent 에 대한 래퍼는 제공하지 않는다(이들 Win32
WaitHandle 클래스 주요 멤버
이름 타입 설명
첫번째는 자원에 대한 핸들을 얻을 때 까지 무한히 대기한다. 두번째와 세번째는 지정된 시간까지 대기할 것을
지정하며, 지정된 시간 동안 자원에 대한 핸들을 얻지 못하면 false 를 반환한다. 핸들을 얻었는지의 여부에
대기 시간이 두 가지 종류가 있는데 int 타입은 밀리 초 단위로 대기시간을 지정하며, 지정할 수 있는 시간의
나타낸다. 따라서 보다 정교한 시간 제어가 필요하다면 TimeSpan 을 쓰도록 하지만, 대부분의 경우에 밀리초
단위로 설정한다.
세번째 요소는 핸들을 기다리기 전에 동기화 도메인을 빠져나올지를 지정한다. 대부분의 예제에서 동기화
143
도메인을 사용하지 않기 때문에 false 를 지정하면 된다.
영문 VS.NET 에 있는 MSDN 에는 "Waits for any of the elements in the specified array to receive a
동일하다.
144
WaitHandle 에 대해서 알아보았으니 Mutex 와 WaitHandle 을 어떻게 이용하는지 예제에서 살펴보도록 하자.
여기서 소개할 예제는 MSDN 에도 있으며 설명하기에 가장 좋기에 여기에 소개한다.(사실은 저작권 문제도 있을
것이고, 조금 마음에 들지 않는 부분도 있어서 읽기 쉽도록 다시 작성해봤다. 겉모양은 달라도 로직은 같다.)
이름 : MutexWait.cs
using System;
using System.Threading;
145
Thread thread3 = new Thread(new ThreadStart(ap.DoSomething3));
IAutoResetEvent[0] = are1;
IAutoResetEvent[1] = are2;
IAutoResetEvent[2] = are3;
IAutoResetEvent[3] = are4;
thread1.Start();
thread2.Start();
thread3.Start();
thread4.Start();
Thread.Sleep(3000);
gM1.ReleaseMutex();
Thread.Sleep(2000);
gM2.ReleaseMutex();
Thread.Sleep(2000);
146
WaitHandle.WaitAll(IAutoResetEvent);
IMutex[0] = gM1;
IMutex[1] = gM2;
Mutex.WaitAll(IMutex);
are1.Set();
gM1.WaitOne();
are2.Set();
147
}
IMutex[0] = gM1;
IMutex[1] = gM2;
Mutex.WaitAny(IMutex);
are3.Set();
gM2.WaitOne();
are4.Set();
148
using System;
using System.Threading;
클래스에서 Mutex 와 Event 를 static 으로 선언한다. static 으로 선언하면 인스턴스 수준에서 자원을 공유할 수
있게 된다. 쓰레드 지역 저장소(Thread Local Storage, TLS)에 대해서는 나중에 설명할 것이다.
149
두 개의 Mutex 를 사용하여 WaitOne, WaitAny, WaitAll 이 어떻게 동작하는지 살펴볼 것이고, 4 개의 쓰레드의
IAutoResetEvent[0] = are1;
IAutoResetEvent[1] = are2;
IAutoResetEvent[2] = are3;
IAutoResetEvent[3] = are4;
Mutex 를 생성하는데 true 를 사용하면 현재 Mutex 를 생성하는 쓰레드가 초기 소유권을 갖게 된다. 여기서는
Main 쓰레드에서 생성하므로 Main 쓰레드가 뮤텍스 gM1, gM2 에 대한 소유권을 갖게 된다.
thread1.Start();
thread2.Start();
thread3.Start();
thread4.Start();
4 개의 쓰레드를 시작하고 Mutex gM1 과 gM2 의 소유권이 메인 쓰레드에 있다는 것을 콘솔에 출력한다.
Thread.Sleep(3000);
모든 쓰레드가 시작된 다음에 Main 쓰레드를 3 초동안 대기시킨다. 3 초 동안 대기되는 동안 각각의 쓰레드는
150
gM1.ReleaseMutex();
Thread.Sleep(2000);
뮤텍스 gM1 을 해제한다는 메시지를 콘솔에 출력하고 ReleaseMutex()를 호출하여 해제한다. 뮤텍스 gM1 을
gM2.ReleaseMutex();
Thread.Sleep(2000);
WaitHandle.WaitAll(IAutoResetEvent);
IMutex[0] = gM1;
IMutex[1] = gM2;
Mutex.WaitAll(IMutex);
are1.Set();
151
첫번째 쓰레드 thread1 에서 실행하는 메소드 DoSomething1 은 두 개의 뮤텍스를 소유할 때만 일을 처리한다.
끝났음을 알려주는 것이다. are1.Set()은 이벤트를 비신호상태에서 신호상태로 변경한다. 이벤트에 대해서
gM1.WaitOne();
are2.Set();
IMutex[0] = gM1;
IMutex[1] = gM2;
Mutex.WaitAny(IMutex);
152
Console.WriteLine("DoSomething3 finished, Mutex.WaitAny(Mutex[])");
are3.Set();
thread3 에서 실행하는 메소드 DoSomething3 은 IMutex 에 지정된 뮤텍스 gM1 과 gM2 를 기다린다. 여기서
IMutex 는 실제로 뮤텍스를 생성하는 것이 아니라 뮤텍스에 대한 컨테이너 역할을 하기 때문에 필자는
인터페이스를 뜻하는 I 를 사용하였다. 앞에서 얘기한 것처럼 실제로 생성된 코드를 살펴보면 뮤텍스 gM1,
gM2.WaitOne();
are4.Set();
이것으로 위 예제의 전체 소스를 살펴보았다. 그러면 위에서 살펴본 예제와 Wait 메소드에 대해서 알아보도록
하자.
153
예제에서 알 수 있는 것들
위 예제에 대해서 이제 몇 가지를 더 생각해 보도록 하자. 먼저 각각의 쓰레드들은 뮤텍스를 이용할 수 있을 때
Main 에서는 뮤텍스를 소유하고 있다가 명시적으로 ReleaseMutex 를 사용하여 뮤텍스의 소유권을 해제하여
다른 쓰레드들이 뮤텍스를 이용할 수 있도록 하였다. 다른 쓰레드에서는 뮤텍스를 해제하는 메소드를 호출하지
실제로 WaitAll 은 모든 자원을 동시에 이용할 수 없으면 자원을 소유하지 않는다. 따라서 Main 이 처음에
gM1 을 해제했을 때 DoSomething1 에서는 gM1 을 소유하지 않는다. 마찬가지로 WaitOne 과 WaitAny 는 어떤
것이든 하나의 핸들에 대한 소유권을 얻으면 동작하기 때문에 문제없이 동작한다. 따라서 Main 이 뮤텍스
같은 결과가 나오겠지만)
실행하는 DoSomething1 이 경쟁하게 된다. 왜냐하면 gM2 가 해제될 때는 WaitAll 에 지정된 두 뮤텍스 gM1 과
라는 질문을 생각해보자.
프로세스나 쓰레드 객체에 대해서 부작용이 있다. 10 개의 쓰레드가 WaitForSingleObject 를 호출하고, 동일한
154
객체에 대한 핸들을 얻을 때 까지 기다린다고 하자. 다른 쓰레드가 이 객체을 반환할 때, 객체는 "이제 사용할
수 있음!"을 뜻하는 신호 상태가 된다. 그러면 이 객체를 기다리던 10 개의 쓰레드가 동시에 일어나서 작업을
수행한다.
위와 같은 상황에서 객체가 "이제 사용할 수 있음!"을 뜻하는 신호 상태가 되었을 때, 한 쓰레드가 깨어나면
AutoResetEvent 를 뜻한다)
이와 같은 이유 때문에 Win32 API 에는 약간의 모호함이 있었고, 닷넷에서는 이것을 WaitOne, WaitAny,
식사하는 철학자
이 예제는 먼저 Table 클래스가 있고, 5 개의 포크(뮤텍스)를 갖고 있다. 각각의 철학자를 뜻하는 Philosopher
클래스는 이 테이블 클래스를 이용하여 포크를 얻어서 식사도 하고, 생각을 하기도 한다. 각각의 식사 시간과
생각하는 시간을 불규칙적으로 하기 위해 Random 클래스를 사용하여 1 부터 100 사이의 임의의 시간을
이용하도록 했다.
이름 : dining.cs
155
using System;
using System.Threading;
namespace hanbit
class Table
get
return bContinue;
set
156
{
bContinue = value;
bContinue = false;
public Table()
bContinue = true;
gFork[0] = gM1;
gFork[1] = gM2;
gFork[2] = gM3;
gFork[3] = gM4;
gFork[4] = gM5;
IFork[0] = gFork[threadID];
157
WaitHandle.WaitAll(IFork);
gFork[threadID].ReleaseMutex();
gFork[(threadID + 1) % 5].ReleaseMutex();
class Philosopher
this.ThreadID = threadId;
this.aTable = table;
Thread.Sleep(rand.Next(1, 200));
158
}
Thread.Sleep(rand.Next(1, 200));
while (aTable.Continue)
aTable.GetForks(ThreadID);
//Eat
this.Eat();
aTable.DropForks(ThreadID);
//Think
this.Think();
~Philosopher()
159
class AppMain
IThread[loopctr].Start();
Thread.Sleep(5000);
table.Stop();
Thread.Sleep(1000);
Console.Read();
160
소스 코드를 살펴보도록 하자
class Table
161
get
return bContinue;
set
bContinue = value;
Table 클래스를 선언하고, 5 개의 뮤텍스를 생성한다. 생성하는 쓰레드에서 뮤텍스의 소유권을 갖도록 할 것이
아니므로 false 를 사용하여 생성한다. gFork 는 5 개의 뮤텍스에 대한 컨테이너로 사용되고, 식사하는 철학자
문제에서 포크를 뜻한다. bContinue 는 식사하는 철학자 프로그램을 계속 실행할지를 결정하기 위해 사용한다.
bContinue = false;
public Table()
bContinue = true;
gFork[0] = gM1;
gFork[1] = gM2;
gFork[2] = gM3;
gFork[3] = gM4;
gFork[4] = gM5;
162
}
Stop 은 bContinue 를 false 로 설정하고, 식사하는 철학자 프로그램을 끝내기 위해 호출한다. Table
IFork[0] = gFork[threadID];
WaitHandle.WaitAll(IFork);
gFork[threadID].ReleaseMutex();
gFork[(threadID + 1) % 5].ReleaseMutex();
테이블에 내려 놓는 것(뮤텍스에 소유권을 해제하는 것)을 나타낸 것이다. IFork[0]에는 철학자의 왼쪽에 있는
포크를, IFork[1]에는 철학자의 오른쪽에 있는 포크를 들도록 한 것이다. 마찬가지로 식사가 끝나면 철학자의
class Philosopher
163
public Philosopher(int threadId, Table table)
this.ThreadID = threadId;
this.aTable = table;
이것은 철학자 클래스이며, 식사하는 시간과 생각하는 시간을 임의의 숫자로 할당하기 위해 Random 클래스를
사용한다. 항상 다른 숫자를 얻기 위해 시스템 시간을 토대로하여 난수를 생성하도록 한다. 철학자 클래스
Thread.Sleep(rand.Next(1, 200));
Thread.Sleep(rand.Next(1, 200));
각각의 철학자는 생각하는 것과 식사하는 것을 하므로, 이를 묘사하는 두 개의 함수를 정의한다. 각각의 함수는
while (aTable.Continue)
aTable.GetForks(ThreadID);
//Eat
164
this.Eat();
aTable.DropForks(ThreadID);
//Think
this.Think();
실제로 철학자의 역할을 반복하는 함수다. Table.Continue 가 true 인 동안은 무한히 반복해서 실행한다. 포크를
class AppMain
IThread[loopctr].Start();
Thread.Sleep(5000);
table.Stop();
Thread.Sleep(1000);
165
Console.WriteLine("Primary Thread ended.");
Console.Read();
Main 클래스로 다섯 명의 철학자가 사용할 table 을 만들고, 5 명의 철학자를 IPhil [] 컨테이너에 할당한다.
실행하라는 의미를 갖는다. 5 초 동안 실행한 다음에는 식사하는 철학자 문제를 끝내기 위해 table.Stop()을
아마도, 위 식사하는 철학자 문제에서 철학자들이 제대로 두 개의 포크를 들고서 식사하는지 궁금할지도
IFork[0] = gFork[threadID];
WaitHandle.WaitAll(IFork);
166
사실대로 말하자면 위 예제에서는 반드시 철학자의 오른쪽과 왼쪽에 있는 포크를 들지는 않는다. 정확히는
테이블 가운데에 5 개의 포크가 있고, 식사가 끝나면 다시 테이블 가운데에 2 개의 포크를 돌려놓는다.
포크를 기다리는 방법
위 철학자 문제에서 포크를 기다리는 방법은 실제로 여러가지가 있다. 위 방법은 그 중에 한 가지에 불과하다.
가장 괜찮은 방법인 것 같다.(더 좋은 방법들도 있겠지만, 필자는 그만큼 알지는 못한다) IAsyncResult 의
디버깅에 대하여
싶다면 디버거를 이용해서 확인할 수 있다. 닷넷 프레임워크는 두 개의 훌륭한 디버거를 제공한다. 관심있는
독자는 찾아보기 바란다. 각각의 디버거의 자세한 사용법은 MSDN 이나 밑에 있는 참고 자료를 참고하기 바란다.
167
VS.NET 에 있는 윈도우 구성요소 업데이트(Windows Component Update)에서 설치한 경우에는 C:\Program
b 56
go
p vars
reg
따라 위치가 다르다.
168
C:\Program Files\Microsoft.NET\FrameworkSDK\GuiDebug 또는 C:\Program Files\Microsoft Visual
이것은 완전한 GUI 환경을 제공하는 디버거다. 디버그 | 디버깅할 프로그램을 선택해서 디버깅할 프로그램을
주의할 점이 있는데, 멀티 쓰레드 응용 프로그램의 경우에 중단점에서 디버거가 멈춰 있어도 계속해서 실행되기
때문에 잠시 머뭇거린 사이에 응용 프로그램이 종료된다. 따라서 Main 에서 Thread.Sleep 을 충분히 늘려놓고
169
사용법을 탐색해 보거나, 아니면 다른 응용 프로그램으로 먼저 연습해 보기 바란다. 지역 창에서 gM1 -
gM5 까지의 트리와 IFork 의 트리를 충분히 펼쳐보면 각각의 핸들(Handle) 값을 볼 수 있으며, 현재 IFork 에서
스택 프레임이 무엇인지 알 수 있다. CLR 디버거는 CLR 환경에 대해서만 디버깅할 수 있는 단점은 있지만
VS.NET 이 있다면 VS.NET 에 있는 디버거를 사용하는 것도 좋다. VS.NET 의 디버거는 비 CLR 환경뿐만 아니라
것이다.
마치며
조금은 정신이 없었을 지도 모르겠다. 이번시간에는 비교적 긴 두 개의 예제를 살펴보았다. 뮤텍스, 이벤트를
살펴보았으며, Wait 함수들이 갖는 의미에 대해서 살펴보았다. 실제로 위에 작성된 예제는 매우 깨지기 쉽다. try
필요할 것이다. 다음 시간에는 지금까지 미뤄왔던 가장 기본적인 동기화 클래스인 Interlocked 에 대해서
알아보고, Monitor 와 Mutex 의 주요 멤버들을 정리해보도록 하자. (앞으로 갈 길이 멀다. ^^;) 궁금한 사항이나
170
C# 쓰레드 이야기 - 13. Interlocked, Heap
지난 시간까지 다양한 동기화 객체에 대해서 살펴보았다. 이렇게 살펴본 것들에는 뮤텍스, 이벤트, 모니터등이
있다. 힙에 대해서 살펴볼 것이며, 닷넷 환경에서 최적화를 위해 어떤 것들을 주의해야 하는지 설명할 것이다.
Interlocked 클래스
Interlocked 클래스는 int 형 값을 증가시키거나 감소시키는데 사용한다. 멀티 쓰레드 환경에서 하나의 int 형
지금까지 이러한 자원의 동기화를 위해서 모니터나 뮤텍스를 사용하는 방법을 설명했지만 간단한 int 형의 값을
메소드 이름 설 명
표에서 여러분은 Increment 와 Decrement 메소드를 가장 자주 사용할 것이다. 이들 메소드의 오버로드 목록은
다음과 같다.
171
public static int Decrement(ref int);
키워드는 참조를 전달하는 것의 의미한다. 다음 예제와 같이 ref 키워드로 전달된 변수는 메소드를 수행한
int age;
age = 21;
DoSomething(age);
Console.WriteLine(age.ToString());
Age = 31;
Win32 API 에서는 Interlocked 동작을 정의하는 3 개의 API 를 제공한다. 이들 API 는 다음과 같다.
172
비트 정수형(System.Int64)를 제공하며, Win32 API 는 32 비트 정수형(LONG)만 제공한다는 점에 주의한다.
Interlocked 클래스 예제
이름 : interlock.cs
using System;
using System.Threading;
ap.DoTest();
173
thread1.Start();
thread2.Start();
Interlocked.Increment(ref m_member);
Thread.Sleep(rdm.Next(1, 200));
Interlocked.Decrement(ref m_member);
Thread.Sleep(rdm.Next(1, 300));
174
Incrementer 메소드를 위임받은 thread1 은 1 을 10 번 증가시키고, Decrementer 메소드를 위임받은 thread2 는
어렵다. 12 번째 글에서 소개한 것처럼 디버거 수준에서 확인하거나 2 개 이상의 CPU 가 장착된 시스템에서
Interlocked.Increment(ref m_member);
175
Thread.Sleep(rdm.Next(1, 200));
사용한 것이다. 여러분이 난수를 생성하기 위해 시스템에서 가져오는 시간값은 1/3 ms 초마다 갱신되기 때문에
있다) ~는 NOT 연산을 수행하며, 비트를 변경한다. unchecked 는 가져온 값을 int 형으로 가져올 때 발생하는
오버플로우 검사를 수행하지 않도록 한 것이다. unchecked 를 사용하지 않으면 OverflowException 예외가
발생한다.
Interlocked 의 함정
m_member++; // 잘못된 방법
Interlocked.Increment(ref m_member);
액세스해야한다.
176
Interlocked 클래스를 사용할 때 알아야 할 점은 하나의 쓰레드가 변수 값을 읽는 동안에 다른 쓰레드가
쓰레드 9 번째 글에서 구현한 C++ 코드와 설명을 참고로 읽어 본다면 개념적인 이해에 도움이 될 것이다. 공유
힙(Heap)
아닐까하고 의심해본다.(정말일까?)
제공한다.
컬렉터가 자원을 관리하기 때문에 힙에 대한 함수를 직접 제공하지 않지만 Win32 API GetProcessHeap()
using System.Runtime.InteropServices;
177
public unsafe class Memory
[DllImport("kernel32")]
많은 함수들이 임시 메모리 블록을 필요로 하며, 이러한 블록들은 기본 힙에서 할당된다. 일반적으로 C 의
printf() 함수를 호출하는데 500 바이트 정도가, Win32 API 호출에는 2k 정도의 메모리가 필요하다. 닷넷에서
할당하려고 하면, 하나의 쓰레드만 메모리 블록을 할당받을 수 있고, 다른 쓰레드는 첫번째 쓰레드의 블록이
할당될 때 까지 기다려야한다. 첫번째 쓰레드의 블록이 할당되면 두번째 쓰레드가 힙을 할당받을 수 있도록
힙을 관리하기 때문에 힙을 직접 제어하거나 하지는 않는다. 그러나 닷넷에서 힙을 관리하는 방법을 이해한다면
힙의 구조
할당자는 Win32 힙 할당자에서 관리하고, Win32 힙 할당자는 NT 런타힘 힙 할당자에서 관리하며, 여기서 가상
메모리를 할당한다.
178
힙 할당자는 8 바이트에서 1024 바이트까지 128 개의 할당자로 구성된다. 힙을 할당 할 때 사용 가능한 블록을
메모리의 사용이 끝나면 힙을 반환한다. 프로세스 힙은 기본적으로 병합을 수행한다. 즉, 인접한 힙이 비어있는
얘기지만 닷넷 런타임은 큰 블록을 할당하여 자체 메모리 관리를 수행한다. 닷넷은 하나의 큰 연속된 공간을
힙은 128 개의 할당자중에 127 개의 할당자를 사용하고, 첫번째 할당자는 다른 용도로 사용한다. 이들 할당자는
힙의 성능저하
할당작업
해제작업
임의의 액세스가 발생하고, 힙이 할당되는 경우에 캐시 누락이 발생하고 성능이 저하될 수 있다.
경쟁
힙 손상
179
대부분의 경우에 닷넷에서 관리하기 때문에 프로그래머에 의해 힙 손상이 발생하는 경우는 없지만
해제된 블록을 다시 해제하는 경우, 해제한 블록을 사용하려는 경우, 블록 경계를 벗어나거나 다른
힙 성능 개선
대신에 128 개의 할당자에서 이용할 수 있는 목록을 별도로 관리하는 Lookaside 목록을 사용하여 힙 성능을
개선했다.(여전히 할당 캐시도 유효한 방법이지만, 닷넷에 대해서만 논하도록 하자. 보다 관심이 있는 분들은
할당 캐시, MP 힙에 대해서 찾아보기 바란다) 대부분의 경우는 가비지 컬렉터에 맡기면 되지만 코드에서 몇
가지 최적화를 할 수 있다. 이러한 최적화는 초당 1000 개 이상의 요청을 처리하는 경우에 뚜렷한 차이가 난다.
박싱과 언박싱은 스택과 힙 사이를 데이터가 오가는 것이기 때문에 많은 오버헤드가 발생한다. 만약
잦은 할당을 최소화
닷넷에서 메모리 관리를 대신하기 때문에 직접 메모리를 할당할 필요가 없지만, 내부적으로는 메모리
설정된 문자열을 변경할 수 없다) 따라서 문자열 연결 작업을 최소화하거나 StringBuilder 클래스를
쓰레드 고유 힙 생성
쓰레드가 빠른 읽기와 쓰기를 필요로 하고, 적은 공간의 메모리 블록을 필요로 하는 경우에 쓰레드
180
고유 힙을 생성할 수 있다. 닷넷에서는 이것을 직접 지원하지 않으며 Win32 API HeapCreate()를
마치며
관리하는 방법, 힙의 문제점과 성능을 개선할 수 있는 방법에 대해서 알아보았다. 여기서 힙을 설명한 이유는
멀티 쓰레드 응용 프로그램의 경우에 자원에 대한 경쟁이 발생하며, 동시에 많은 요청을 처리할 경우에 심각한
성능 문제로 이어지기 때문이다. 따라서 응용 프로그램을 작성할 때 힙을 어떻게 최적화할 것인지 결정해야
한다.
다음에는 쓰레드 풀링에 대해서 알아볼 것이며, 그 이후에는 다양한 동기화 클래스들과 비동기 처리 및 웹
서비스에서의 비동기 처리에 대해서도 설명할 것이다.(꽤 길다~) 끝까지 읽고, 직접 코드를 테스트하신
참고
• http://msdn.microsoft.com/net/sscli
정보를 얻을 수 있다.
181
C# 쓰레드 이야기 - 14. 마지막 이야기
시간에는 그동안 설명하지 못했던 부분들과 미흡했던 부분들, 그리고 쓰레드에 대해서 생각해 보아야 할
문제점을 비롯하여 닷넷에서 제공하는 쓰레드 프로그래밍의 문제점과 앞으로의 방향에 대해 함께 생각해보도록
하자.
교착상태의 조건
• 여러 리소스들의 소유와 대기
• 상호 배제(Mutual Exclusion)
• 무선점(No Preemption)
• 순환 대기(Circular wait)
그렇지만 두 번째와 세 번째는 운영체제에서 보장해 주는 사항이기 때문에 신경쓰지 않아도 된다. 흔히들
배제와 무선점을 보장해준다. 결국 프로그래머가 책임질 부분은 첫번째와 네 번째가 되며, 지금까지 이러한
상태를 해결하는 방법은 여러 가지가 있지만 이중에서 대표적인 것을 나열하면 다음과 같다.
• Detect
• Prevent
• Ignore
많이 걸리기 때문에 실제로 잘 사용하지는 않지만 디버깅과 크리티컬한 서버 시스템을 프로그래밍하는 경우에
주로 사용한다. 실제로 이 방법을 구현하려면 우리가 사용하고 있는 x86 프로세서의 하드웨어적인 구조를 잘
182
알고 있어야 하며, 어셈블리에 대한 지식과 x86 op-code 에 대한 지식도 필요하다. 따라서 일반적으로
예방하기 위한 방법을 설명한 것이다. 또한, 교착상태를 예방하는 경우는 제한된 리소스에 대한 접근을
형태로 제공하고 있으며, 이에 대한 C++ 구현을 이전 연재 기사에서 소개했었다. 그러한 코드를 응용하여
Mutex 에 대해서 소개하면서 꽤 장황하게 설명한 느낌이 들었다. 하지만 그러면서도 제대로 설명하지 못했다는
동기화 보다는 그 속도가 느리다. 그러나 커널 객체이기 때문에 응용 프로그램 프로세서 경계 건너편에
있지만 실제로 두 핸들이 동일한 주소 0x8000 0000 를 가리킬 수 있다는 것이다. 이들 핸들은 32 비트
정수형이다. 앞의 기사에서 설명한 뮤텍스 예제에서는 뮤텍스에 대한 이름을 지정하지 않았기 때문에 각 응용
프로그램간에 독립적이었으며, 같은 뮤텍스를 소유하게 하려면 Named Mutex 를 사용한다. 복잡한 것은 아니고
183
앞에 연재한 기사들 중에서 Process 클래스에 대해서 설명했고, 시스템에 있는 프로세스 정보를 출력하는
간단한 예제를 제시한 적이 있다. Process 클래스에 대한 자세한 내용은 MSDN 을 참고하길 바라지만 자주
• Start, Stop
• Handle
• WairForExit
Start 와 Stop 을 사용해서 프로세스를 시작하고 종료할 수 있다. Win32 환경에서 프로세스는 쓰레드에 대한
있으며, Named Mutex 를 복제하여 응용 프로그램의 Mutex 로 지정해 줄 수도 있다. WaitForExit 는 프로세스가
파일을 다운 로드하거나 레코딩 작업, 디스크 정리 작업등을 끝낸 다음에 자동으로 응용 프로그램을 종료하거나
쓰레드 풀링에 대해서 자세히 설명할 기회를 갖지 못한 것을 아쉽게 생각한다. 쓰레드 풀링은 시스템의 자원을
IIS 와 같은 웹 서버는 사용자가 웹 페이지를 요청할 때 쓰레드를 생성하고, 웹 페이지에 대한 요청을 완료하면
따라서 서버는 50 개 정도의 쓰레드를 쓰레드 풀(Thread Pool)에 보유하고 사용자의 요청이 들어오면 쓰레드
풀에 있는 자유 쓰레드(Free Thread)가 쓰레드 풀 밖으로 나와서 요청을 처리하고, 나머지 작업에 대해서는
작업자 쓰레드(Worker Thread)에 일임하고 다시 쓰레드 풀로 돌아오는 방식을 취한다. 여기서 작업자 쓰레드는
184
실제 쓰레드가 아니기 때문에 의사 쓰레드 또는 가짜 쓰레드(Pseudo Thread)라고 얘기한다. 사용자가 의사
이러한 과정에서 알 수 있는 것처럼 쓰레드 풀링은 긴 처리시간을 갖는 쓰레드에 대해서는 그다지 적합하지
않으며, 생성과 제거 과정이 빈번한 멀티 쓰레드 프로그래밍에 적합하다. 이외에 쓰레드 풀링이 제공하는
쓰레드 풀링의 장점
• 제한된 시스템 자원 사용
• 적은 CPU 시간 소모
• 사용자 UI 의 응답성 향상
쓰레드 풀링의 단점
이러한 얘기들은 쓰레드 풀링에 국한된 것이 아니며, 쓰레드 프로그래밍 자체에 대한 것이다. 오히려, 쓰레드
185
• 실행 시간이 오래 걸려서 다른 작업을 차단해야 하는 경우
• 쓰레드 ID 가 필요한 경우
쓰레드 풀링 주요 메소드
같다.
class TestPoolApp
ThreadPool.QueueUserWorkItem(new
WaitCallback(TestCallback),str);
Thread.Sleep(2000);
Console.WriteLine(state);
타이머
186
타이머는 여러분이 생각하는 것과 달리 시스템과 독립되어 있다. 일반적인 시스템에 장착되어 있는 타이머는
다음과 같이 나눌 수 있다.
• 실시간 시계: 컴퓨터의 메인 보드에 내장되어 있으며, 수은 전지를 통해서 째깍째깍 돌아가는 진짜
시계를 뜻한다.
• 8253 칩: 8253 칩에서 시스템 클럭을 1 초에 120 만번을 만들어 내며, 이 클럭은 16 비트 레지스터에서
닷넷에서의 타이머
클래스이며, 55ms 의 한계를 갖지만, 일반적인 용도로는 무리가 없다. 그러나 윈도우 타이머는 시스템의 다른
쓰레드 작업에 의해서 영향을 받을 수 있기 때문에 멀티 쓰레드 프로그래밍에는 적합하지 않은 경우가 많다.
위에서 소개한 8253 타이머에서 발생하는 클럭은 CPU 와는 무관하게 하드웨어에서 발생하는 것이다. 즉,
시스템이 바쁜지에 관계없이 항상 일정하게 클럭을 발생시킨다. 이와 같은 클럭을 사용하여 보다 정교한 제어가
필요한 곳에 사용할 수 있다. 이러한 클럭을 제공하는 타이머는 System.Timers.Timer 클래스에서 제공한다.
어떤 타이머를 사용하는 가는 독자의 선택이며, 경우에 따라 적절한 것을 사용하면 된다. VB6 프로그래머나
쓰레드 예외 처리 클래스 계층
쓰레드 예외 처리 클래스 계층 구조에 대해서 미흡하게 설명한 감이 있어서 여기에 그 클래스 계층 구조를
187
SystemException
- ThreadAbortException
- ThreadInterruptedException
- ThreadStateException
- SynchronizationLockException
MulticastDelegate
- IOCompletionCallback
- ThreadExceptionEventHandler
- ThreadStart
EventArgs
- ThreadExceptionEventArgs
ApartmentState
사실이다. 굳이 여기서 VB6 를 언급하는 것은 VB6 로 작성한 컴포넌트를 닷넷에서 사용할 경우에는 이
ApartmentState 를 특별히 지정할 필요가 있다. 이 속성은 COM 과의 하위 호환성을 위해서 제공된다고
생각하면 된다.
STA 와 MTA
ApartmentState 는 STA 와 MTA 구조가 있는데, Single Threaded Apartment 라는 것은 하나의 쓰레드가
아파트를 소유하는 구조를 갖는다. 즉, 멀티 쓰레드를 제공할 수 없는 구조로 되어 있다. MTA 는 Multi
188
Threaded Apartment 로 되어 있으며, 하나의 아파트에 여러 쓰레드가 세들어 살 수 있는 구조를 갖으며, 각각의
Thread.CurrentThread.ApartmentState = ApartmentState.MTA;
Thread.CurrentThread.AprartmentState = ApartmentState.STA;
닷넷에서는 MTAThread 가 기본 설정이며, 이는 COM 객체와도 잘 맞는다. 다만, VB6 로 작성한 DLL 에
대해서만 호환성을 위해 STAThread 를 사용하여 COM 객체에 대한 현재 쓰레드를 STA 로 설정하도록 해야한다.
Reader/Writer Model
사용하여 데이터를 생산하고, 데이터를 소비한다면 데이터 생산과 소비가 하나로 연결되어 있기 때문에
효율적으로 처리할 수 없을 것이다. 이와 경우에 쓰레드 사용시 발생하는 부하까지 고려한다면 더 비효율적이다.
따라서 생산자와 소비자를 각각의 쓰레드로 분리하여, 생산자 쓰레드는 최대한의 데이터를 생산해내고, 소비자
쓰레드는 최대한 데이터를 소비하는 구조가 보다 효율적일 것이다. 닷넷에서는 이들을 ReaderWriterLock
속성
- IsReaderLockHeld
- IsWriterLockHeld
- WriterSeqNum
메소드
- AcquireReaderLock
189
- AcquireWriterLock
- ReleaseReaderLock
- ReleaseWriterLock
• 단일 생산자/단일 소비자
• 단일 생산자/다중 소비자
• 다중 생산자/단일 소비자
• 다중 생산자/다중 소비자
단일 생산자/단일 소비자
이러한 모델들은 서로간에 데이터를 공유하지 않는다. 이벤트에서 설명한 예제에서와 같이 한 쓰레드가
데이터를 읽어서 처리하고, 처리가 끝난 데이터를 다른 쓰레드에 넘겨주는 형태를 띄게 된다. 즉, 생산자와
소비자 간에 데이터가 있음과 없음을 알려주는 이벤트를 사용하는 형태가 된다. 이러한 단일 생산자/단일
이러한 pipe-lined 처리의 대표적인 예로는 멀티미디어 플레이어가 있다. 멀티미디어 플레이어는 데이터를 읽어
들이고, 데이터를 디코딩하고, 데이터를 화면에 표시하는 것으로 나눠진다. 이들 쓰레드는 데이터를 공유하지
않으며 처리되는 데이터를 넘겨받는다. 즉, A → B → C 순으로 각 쓰레드가 처리한 데이터를 넘기는 형태를
띈다.
경우에 소수를 찾아서 버퍼에 저장하는 쓰레드와 버퍼에 데이터가 있는지 검사하여 화면에 데이터를 뿌려주는
190
특히 소수를 찾는다고 했을 때 2 로 나누어지는지 확인하는 쓰레드, 3 으로 나누어지는지 확인하는 쓰레드, 5 로
나누어지는지 확인하는 쓰레드, 그리고 7 과 같이 발견된 소수를 소수를 유지하는 버퍼에 집어넣고, 7 로
11 로 나누어지는지 확인하는 쓰레드를 동적으로 생성하는 구조를 작성했다고 하자. (소수에서 짝수는 2 밖에
없으므로 짝수는 무시하고 +2 씩 증가시키면 된다.) 이처럼 각각을 나누는 함수를 쓰레드로 나누고, 추가된
소수에 대해서 확인하는 쓰레드를 동적으로 생성하면 프로그램의 유연성을 높이고, 보다 빠르게 실행되는
바로 다음 단계를 수행할 수 있다. 즉, 각 단계의 처리시간을 1 이라고 산정할 경우에 선형적인 모델은 15 라는
CPU 의 파이프 구조라는 것이 이것을 뜻한다. 이 파이프 구조가 뭐부터 적용되기 시작했는지 기억은 안나지만,
단일 생산자/다중 소비자
우리가 흔히 네트워크에서 접하는 온라인 게임들에 해당한다. 이러한 온라인 게임들은 하나의 서버가 가상의
세계에 대한 실시간 연산을 수행하며, 클라이언트는 이러한 서버의 데이터를 가져와서 읽어들이고 해석하여
적절한 그래픽으로 화면에 뿌려주는 것이다. 온라인 게임 외에 가장 흔히 보는 예로는 온라인 방송도 있다.
다중 생산자/단일 소비자
191
생산자가 여럿이고, 소비자가 하나인 모델에는 어떤 것이 있을까? 가장 일반적인 것은 큐를 생각할 수 있다.
출력 작업은 하나의 프린터에서만 발생한다(단일 소비자). 이러한 구조를 다중 생산자/단일 소비자 모델이라고
다중 생산자/다중 소비자
이것은 생산자와 소비자가 여럿인 경우로 대표적인 것은 항공사의 예약 시스템을 들 수 있다. 각 공항의
데스크와 사무실에서 좌석 예매를 처리할 수 있으며, 고객은 언제든지 온라인에서 남아있는 좌석과 좌석 수를
확인하고 예매를 할 수 있다. 혹시나 이러한 항공사 시스템에 대해서 궁금한 분들은 인터넷에 찾아보면 꽤 많은
자료들이 있고, 재미난 자료들도 있으니 찾아보기 바란다. 80 년대에 IBM 이 행했던 항공사 예매 시스템에 대한
자료도 있으며, 90 년대에 공항 안내 화면에 MS 의 블루 스크린이 떴다는 이야기까지 다양한 것들이 준비되어
있을 것이다. ^^;
동기 대리자에는 Invoke 메소드가 있으며, 비동기 대리자에는 IAsyncResult 인터페이스가 있다. 실제로 웹의
특성상 동기처리는 거의 사용하지 않으며, 웹 서비스와 관련하여 비동기 서비스를 사용하게 된다. 즉, 사용자가
지역과 지역을 지정하면 지역간 최단 이동경로를 표시하면서 두 지역간의 주요 위치 정보를 가져오는 각각의
처리할 수 있을 것이다.
비동기 처리에 쓰이는 메소드는 BeginInvoke 와 EndInvoke 메소드가 있다. 자세한 것은 MSDN 을 찾아보기
바란다. 실제로 동기 처리를 많이 하지 않는다면 IAsyncResult 와 대리자를 사용하여 쓰레드 사용을 캡슐화할
IAsyncResult 인터페이스
192
System.Runtime.Remoting 네임 스페이스에 정의되어 있다. AsyncResult 콜백을 사용하며, IsCompleted 속성을
사용하여 작업의 완료 유무를 확인할 수 있다. AsyncResult 클래스는 비동기 작업을 캡슐화했으며,
WebClientAsyncResult 클래스는 XML 웹 서비스 프록시에서 비동기 메소드 패턴을 구현하는데 사용하기 위해
using System;
using System.Threading;
using System.Runtime.Remoting.Messaging;
return s.Length;
Compute c = (Compute)((AsyncResult)ar).AsyncDelegate;
string s = (string)ar.AsyncState;
193
Compute c = new Compute(TimeConsumingFunction);
Console.WriteLine("Ready");
Console.Read();
비동기 웹 서비스 구현
WebClientAsyncResult 클래스를 이용한다. 아래 코드는 간단히 구현한 비동기 웹 서비스의 일부를 옮긴 것이다.
WaitHandle [] wh = {
arStart.AsyncWaitHandle,
arFinish.AsyncWaitHandle,
arDirections.AsyncWaitHandle
};
WaitHandle.WaitAll(wh);
((WebClientAsyncResult)arDirections).Abort();
194
Never know I'm here.(내가 여기있는걸 절대 모를걸) - Ghost in StarCraft
앞에서 교착상태가 발생하는 네 가지 경우에 대해서 이야기 했고, 그 중 두 가지는 운영 체제가 책임지는
부분이며, 다른 두 가지는 프로그래머가 책임져야 한다고 했다. 또한 프로그래머는 교착상태 검출과 예방에
힘쓰며, 대개의 경우에 예방에 힘쓴다고 했다. 실제로 교착상태를 검출할 수 있는 기능을 닷넷 프레임워크는
교착상태를 검출하려면 다중 프로세서를 사용하는 PC 가 있어야 한다. 그리고 리눅스 서버나 듀얼 메인보드라고
하여 시중에서 판매되는 메인 보드에 일반 펜티엄 III CPU 를 2 개씩 꽂는 것으로는 제대로 테스트할 수 없다.
설치한 시스템이 있어야 한다. 만약, 이와 같은 환경이 갖추어지지 않았다면 자신이 작성한 코드가 싱글
프로세서 머신에서 잘 수행된다고 하더라도, 다중 프로세서 머신에서는 제대로 수행되지 않는다고 생각하기
바란다.
있다. 윈도우 NT/2000/XP 환경이라면 set 을 입력하면 PROCESSOR_*로 시작하는 환경 변수에서 CPU 정보를
얻을 수 있다.)
이제, 검출에 대해서 이야기 해보자. 교착상태 검출이라는 것은 특별한 것은 아니다. 메모리에 로드되어 특정
함수가 호출되는가를 감시하는 것이다. 그리고 이러한 함수들을 호출하는 것을 발견하면 해당 쓰레드 ID,
쓰레드의 핸들(32 비트 값이며, 보통 어드레스), 입력값, 반환값, 호출/반환등을 간단히 로그파일에 기록하는
컴파일시 생성한 디버깅 정보를 이용해서 디버거에서 문제를 동일하게 재현하고, 문제를 해결할 수 있다.
그렇다. 내용을 알고나면 별거 아니다. 그러면 뭐가 문제인가? 실제로는 교착상태 검출을 위해서는 검출 루틴
자체가 메모리에 로드되어야 한다. 또한, 실행 파일 뿐만 아니라 실행 파일이 사용하는 DLL 의 복사본이
195
메모리에 로드된다는 것은 익히 알려진 사실이다. 이처럼 메모리상에 로드된 DLL 에서 특정 함수가 호출될 때를
닷넷에서는 후킹에 대한 메소드를 제공하지 않는다(베타 1 에는 있었지만, 이후에 제거되었다). 따라서 Win32
API 함수를 DllImport 특성을 사용해서 선언하여 사용하도록 한다. (실제로 DLL 에서 호출되는 함수에 대한
어드레스 매핑등에 대한 정보는 .idata 섹션에 저장된다. 이러한 실제 구현에 대해서 궁금하다면 PE File
Format 에 대한 문서를 읽어보고, COFF(Common Object File Format)을 참고하면 도움이 될 것이다. 그리고
마이크로소프트웨어 2002 년도 중에 PE File Format 에 대해서 자세히 설명한 기사가 있었던 것으로 기억한다.)
않고 있다).
후킹을 하려면 WH_*와 같은 윈도우 핸들를 정의하도록 한다. CallNextHookEx 에서 후킹할 것들을 지정할 수
있으므로 원하는 윈도우 핸들을 지정해서 처리할 수 있을 것이다. 셸에 대해서 처리하고 싶다면 WH_SHELL 을,
메시지 처리를 닷넷의 IFilterMessage 대신에 직접 처리하고 싶다면 WH_MSGFILTER 를, 시스템 메시지 처리에
사용할 수 있다. 이러한 후킹을 처리하는데 필요한 구조체를 C#에서 정의하려면 구조체를 정의하는 struct
[StructLayout(LayoutKind.Sequential)]
196
public int dwExtraInfo;
비동기 메소드
Chords
class Buffer
return s;
buff.put("blue");
buff.put("sky");
197
Console.Write(buff.get() + buff.get());
버퍼에 데이터를 집어넣는다. put 메소드는 비동기 메소드이므로 처리가 완료된다. 두 번째 buff.put("sky")도
Console.Write(buff.get() + buff.get());
여기서 get()을 호출한다. chords 로 정의된 메소드는 두 조건이 일치할 때만 수행된다. 즉, 먼저 비동기 메소드
put 을 호출하여 비동기 메소드 조건을 만족시키고, get 을 호출하여 동기 메소드 조건을 만족시킬 때 블록 안의
문장 return s;가 실행된다. 결과는 버퍼에 들어가 있던 "blue"를 꺼내오고, 두 번째는 "sky"를 꺼내온다. 따라서
class Buffer {
return s;
return n.ToString();
이것은 버퍼 클래스에 대해서 오버로딩을 적용한 것이다. 이제 버퍼에는 string 형뿐만 아니라 int 형도 저장할
수 있게 된다.
class OneCell()
contains(o);
198
empty();
return o;
이것은 셀을 사용하여 데이터를 저장하는 형태를 가진다. OneCell 클래스를 초기화할 때 생성자에 있는
있는 contains(o);라는 메시지가 전달된다. 이 메시지는 async contains()에서 받아서 처리하게 된다. get 이
메시지를 주고 받는다고 하지만 객체 지향 언어에는 그 메시지 자체가 숨겨져 있다. 그러나 위에서 볼 수 있는
것처럼 Concurrent Abstraction Pattern 에서는 이러한 메시지를 공개하고, 프로그래머가 직접 정의하고 사용할
class ReaderWriter {
ReaderWriter() { Idle(); }
이것은 ReaderWriter 를 클래스로 구현한 것이다. 다른 부분은 모두 비슷하니 무시하고, public void
ReleaseShared() & async S(int n) 부분만 설명하겠다. 이 부분은 n 이 1 이 될 때까지 계속해서 펌프질을 해서
class Token {
199
public void Grab() & public async Free() {}
class Heavy {
있는 것이 아니라 3 개 이상의 메소드를 조인할 수도 있다. 이러한 메소드를 간단히 Join 메소드라고 부르며,
C# 컴파일러는 이것을 해석하여 일반 클래스로 재작성한다(컴파일 과정일 뿐이다). 이것은 아직까지 소개되지
않았고, 언제쯤 Join Method 가 적용될지는 아무도 모른다. 그러나 Concurrent 에 대한 문제를 해결하기 위한
알려지지 않은 이야기
증명하고, 컴파일러로 구현한 것이 Concurrent Pascal이다. 1975 년에 등장한 이 Concurrent Pascal은 플랫폼에
효율적으로 처리할 수 있다. 그러나 많은 언어들이 Concurrent Pascal의 아이디어를 채용하고 있는 것은 아니다.
이중에 쓰레드와 동기화를 제공하는 언어에는 자바와 닷넷 플랫폼이 있다.(닷넷 플랫폼 기반의 언어는 모두
200
그러나 자바에서는 모니터와 같은 핵심적인 구현을 생략했기 때문에 Concurrent 분야에 대한 지난 25 년간의
제공하고 있지만, 쓰레드에 대한 여러 가지 클래스와 메소드들의 의미가 상당히 모호하다는 평가를 받고 있다.
자바는 스펙에서 쓰레드에 대한 예외 처리 부족과 매우 빈약한 지원으로 공격당하고 있다. 마찬가지로 닷넷의
쓰레드는 모두 Win32 API 함수에 대한 단순한 래퍼(wrapper)로 되어 있으며, Win32 API 쓰레드의 고질적인
문제점 '당신이 생각한 데로 정확하게 동작하지 않는다'라는 문제점을 지적받고 있다. 사실, Win32 API 전체가
PThread)에서는 WaitForMultipleObjects를 사용하지 않고도 문제를 유연하게 처리할 수 있다. 실제로 이러한
독자들은 이제 슬슬 궁금해질 것이다. "trax씨, 당신은 지금까지 닷넷 쓰레드에 대해서 설명해오지 않았나요?
있다. 자바와 닷넷 플랫폼이 나름대로의 장단점을 갖고 있으며, 단점이 있으면 그것을 피해나갈 수 있을 것이다.
사실, 닷넷 플랫폼은 Win32 쓰레드의 문제점을 해결하고, 의미를 명확하게 하려 했지만, 반은 성공, 반은
실패했다고 생각한다. 문제를 명확하게 나눈 부분도 있지만, Win32 API에 대한 생각이 지배적인지 Win32 API의
사실, 자바의 부족한 쓰레드 지원과 동기화 모델의 부족을 해결하기 위해서
플랫폼을 위한 C++ 스레딩 패키지였다. 현재는 자바 버전도 함께 제공되기 시작하고 있으며, 사실상 자바에서
있다. 이것은 Unix/Linux 환경의 pthread를 Win32 환경에 구현한 것으로 객체 지향, 크로스 플랫폼을 위한
201
스레딩 패키지이며, C/C++에서 사용할 수 있다. 마찬가지로, DllImport를 사용해서 닷넷 플랫폼에서 사용할 수
있지만, 관리되지 않는 코드(unamanged code)를 사용하는 것에 대해서는 의견이 분분하다. 자바와 마찬가지로
특정 언어가 제공하는 스레딩 지원을 떠나 다양한 플랫폼에 적용할 수 있는 POSIX Thread를 접해보는 것도
중요하며, 자신이 사용하는 언어에서 제공하는 스레딩 모델의 장단점을 객관적으로 바라보고, 그것을 이해하여
쓰레드에 대한 많은 구루(guru)들은 자바의 스펙과 스레딩 모델의 지원은 빈약하기 그지없고, 불완전하기
때문에 데이터 훼손(data corruption)을 제대로 막을 수 없다고 비난하고 있고, 닷넷은 다양한 모델을
제공하지만, 생각하는 데로(코딩하는 데로) 동작하지 않는 경우가 많다고 비난하고 있다. 또한, 닷넷 플랫폼의
동기화 예제들 중에 몇몇은 교착상태가 발생하는 것을 예제라고 올려두었다며 심하게 비난하고 있다. (실제로
이러한 코딩은 교착상태가 발견합니다. 라는 예제로 MSDN 사이트의 예제를 인용하고 있으며, 조목조목 잘
설명해 놓았다.) 그리고 구루들은 한결같이 이러한 문제의 대안으로 PThread를 사용할 것을 권하고 있다.
결국, 쓰레드라는 미지의 세계는 어느 한쪽만 알아서도 안되고, 책을 너무 의지해서도 안된다. 많은 현인들의
지혜를 얻어오고 경험을 더 쌓아야 한다는 것을 알 수 있다. 닷넷 플랫폼에서의 쓰레드에 대해서 설명했지만,
구현 예제보다는 운영체제와 관련된 개념을 더 많이 전달하고 싶었는데, 제대로 되지 않아서 아쉽다. 관심이
많은 분들은 운영체제와 POSIX Thread, Win32 Threading Model, Memory Model에 대해서 학습하기 바란다.
지금까지 쓰레드에 대한 기사를 쓰면서 설명했던 예제나 미처 설명하지 못했던 예제들을 제공하려고 한다.
여기에는 필자가 원격으로 동적으로 IP를 할당받은 머신을 찾기 위해 작성한 멀티 쓰레드 포트 스캐너 같은
간단한 유틸리티도 있으며, 어셈블리(DLL)로 스레딩 코드를 작성하고, ASP.NET에서 멀티 쓰레드 검색엔진으로
모쪼록 부족한 부분이나마 이 글이 닷넷을 포함한 쓰레드 전반에 대한 이해에 도움이 되었으면 싶다. (후일에
202
구루(guru)들에게 감사드린다.
참고자료
POSIX Threads에 대해서 다루고 있으며, 초보자에게 가장 적합한 입문서이다. 실제로 초보자를 위한
제목과는 달리 쓰레드 프로그래밍, 메모리 모델, 예외 처리에 대해서 설명하고 있는 책으로, 저자의
• Modern Concurrency Abstractions for C#, Nick Benton, Luca Cardelli, Cedric Fournet, MS Research
203
• Performance Limitations of the Java Core Libraries, Allan Heydon, Marc Najork, Compaq Systems
Research Center
이곳에서 문서, 프로젝트 소스 코드를 얻을 수 있다. Win32, 자바, UNIX/Linux 환경을 모두 지원한다.
POSIX Threads 프로그래밍에 대한 방대한 정보와 링크를 제공한다. 이곳에서 여러분이 필요한
프로그래밍을 할 수 있다.
• Interprocess Communications
프로세스간 통신에 대한 내용으로 MSDN이나 MSDN Online의 목차에서 검색하기 바란다. 순서는
Services | Memory는 Memory Model에 대한 깊은 설명을 제공할 것이고, 여기서 가상 메모리 관리, 힙,
204