사실, 필자의 심경으로는 밥 쳐먹고 몽상하는 게 전부인데다가, 이 위생 관념도 없는 집단은 남이 쓰던 포크도 마다 않고 쓰는 똘아이 집단을 거창하게 "식사하는 철학자"라고 소개하는 것은 뭔가 불합리하다고 생각한다. -_- |
System.Object System.MarshalByRefObject System.Threading.WaitHandle System.Threading.AutoResetEvent System.Threading.ManualResetEvent System.Threading.Mutex즉, 뮤텍스나 이벤트 모두 WaitHandle 클래스에서 파생된 클래스라는 것을 알 수 있다. WaitHandle은 공유 자원에 대한 배타적 접근 허용을 위해서 제공되는 클래스다. 다시 말해서, WaitHandle은 커널 객체에 대한 동기화를 제공하는 Win32 API에 대한 래퍼 클래스다. WaitHandle 클래스는 추상 클래스이므로 동기화 객체를 위해서 클래스를 상속시킬 수 있다.
이름
|
타입
|
설명
|
Handle | IntPtr | 운영 체제 핸들을 가져오거나 설정한다. |
WaitTimeout | int | 대기 핸들이 일어나기 전에 WaitAny의 제한 시간이 초과했음을 나타낸다. |
Close | void | 핸들이 갖고 있는 자원을 해제한다. |
public virtual bool WaitOne(); public virtual bool WaitOne(int, bool); public virtual bool WaitOne(TimeSpan, bool);첫번째는 자원에 대한 핸들을 얻을 때 까지 무한히 대기한다. 두번째와 세번째는 지정된 시간까지 대기할 것을 지정하며, 지정된 시간 동안 자원에 대한 핸들을 얻지 못하면 false를 반환한다. 핸들을 얻었는지의 여부에 따라서 bool 값 true/false를 반환한다.
public static int WaitAny(WaitHandle[]); public static int WaitAny(WaitHandle[], int, bool); public static int WaitAny(WaitHandle[], TimeSpan, bool);WaitOne과 마찬가지로 두 번째와 세번째는 모두 지정된 시간까지 대기할 것을 지정하며, 지정된 대기 시간 동안 자원에 대한 핸들을 얻지 못하면 false를 반환한다.
한글 VS.NET에 있는 MSDN을 보면 WaitHandle.WaitAny에 대한 설명이 "지정된 배열의 모든 요소가 신호를 받기를 기다립니다."와 같이 되어 있는데, 이는 모호한 설명이다. WaitHandle[]에 2개의 핸들을 지정했을 때, 2개의 핸들 중에 하나를 받으면 쓰레드 대기상태(WaitSleepJoin)가 해제되고 작업을 진행한다(Suspended 또는 Running).
영문 VS.NET에 있는 MSDN에는 "Waits for any of the elements in the specified array to receive a signal."와 같이 되어 있으며, 원문에 대한 오역이라는 것을 알 수 있을 것이다. WaitAny[]에 대해서는 예제에서 설명할 것이다. |
public static int WaitAll(WaitHandle[]); public static int WaitAll(WaitHandle[], int, bool); public static int WaitAll(WaitHandle[], TimeSpan, bool);WaitHandle[]과 같이 지정된 요소에 대한 모든 핸들을 얻을 때 까지 대기하는 점을 제외하면 WaitAny()와 동일하다.
이름 : MutexWait.cs using System; using System.Threading; public class AppMain { static Mutex gM1; static Mutex gM2; static AutoResetEvent are1 = new AutoResetEvent(false); static AutoResetEvent are2 = new AutoResetEvent(false); static AutoResetEvent are3 = new AutoResetEvent(false); static AutoResetEvent are4 = new AutoResetEvent(false); public static void Main() { AppMain ap = new AppMain(); Thread.CurrentThread.Name = "Primary Thread"; Thread thread1 = new Thread(new ThreadStart(ap.DoSomething1)); Thread thread2 = new Thread(new ThreadStart(ap.DoSomething2)); Thread thread3 = new Thread(new ThreadStart(ap.DoSomething3)); Thread thread4 = new Thread(new ThreadStart(ap.DoSomething4)); gM1 = new Mutex(true); gM2 = new Mutex(true); AutoResetEvent[] IAutoResetEvent = new AutoResetEvent[4]; IAutoResetEvent[0] = are1; IAutoResetEvent[1] = are2; IAutoResetEvent[2] = are3; IAutoResetEvent[3] = are4; thread1.Start(); thread2.Start(); thread3.Start(); thread4.Start(); Console.WriteLine("-------- " + Thread.CurrentThread.Name + " - owns gM1 and gM2 " ); Thread.Sleep(3000); Console.WriteLine("-------- " + Thread.CurrentThread.Name + " - releases gM1"); gM1.ReleaseMutex(); Thread.Sleep(2000); Console.WriteLine("-------- " + Thread.CurrentThread.Name + " - releases gM2"); gM2.ReleaseMutex(); Thread.Sleep(2000); WaitHandle.WaitAll(IAutoResetEvent); Console.WriteLine("All threads just have finished"); } public void DoSomething1() { Console.WriteLine("DoDomething1 started, Mutex.WaitAll(Mutex[])"); Mutex[] IMutex = new Mutex[2]; IMutex[0] = gM1; IMutex[1] = gM2; Mutex.WaitAll(IMutex); Console.WriteLine("DoSomething1 finished, Mutex.WaitAll(Mutex[])"); are1.Set(); } public void DoSomething2() { Console.WriteLine("DoDomething2 started, gM1.WaitOne()"); gM1.WaitOne(); Console.WriteLine("DoSomething2 finished, gM1.WaitOne()"); are2.Set(); } public void DoSomething3() { Console.WriteLine("DoDomething3 started, Mutex.WaitAny(Mutex[])"); Mutex[] IMutex = new Mutex[2]; IMutex[0] = gM1; IMutex[1] = gM2; Mutex.WaitAny(IMutex); Console.WriteLine("DoSomething3 finished, Mutex.WaitAny(Mutex[])"); are3.Set(); } public void DoSomething4() { Console.WriteLine("DoDomething4 started, gM2.WaitOne()"); gM2.WaitOne(); Console.WriteLine("DoSomething3 finished, gM2.WaitOne()"); are4.Set(); } }예제를 모두 입력하고 컴파일 한 결과는 다음과 같다.
using System; using System.Threading; public class AppMain { static Mutex gM1; static Mutex gM2; static AutoResetEvent are1 = new AutoResetEvent(false); static AutoResetEvent are2 = new AutoResetEvent(false); static AutoResetEvent are3 = new AutoResetEvent(false); static AutoResetEvent are4 = new AutoResetEvent(false);콘솔로 작성할 것이고, 쓰레드 응용 프로그램이므로 네임 스페이스 System과 System.Threading을 사용한다. 클래스에서 Mutex와 Event를 static으로 선언한다. static으로 선언하면 인스턴스 수준에서 자원을 공유할 수 있게 된다. 쓰레드 지역 저장소(Thread Local Storage, TLS)에 대해서는 나중에 설명할 것이다.
gM1 = new Mutex(true); gM2 = new Mutex(true); AutoResetEvent[] IAutoResetEvent = new AutoResetEvent[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(); Console.WriteLine("--------- " + Thread.CurrentThread.Name + " - owns gM1 and gM2 " );4개의 쓰레드를 시작하고 Mutex gM1과 gM2의 소유권이 메인 쓰레드에 있다는 것을 콘솔에 출력한다.
Thread.Sleep(3000);모든 쓰레드가 시작된 다음에 Main 쓰레드를 3초동안 대기시킨다. 3초 동안 대기되는 동안 각각의 쓰레드는 시작하지만, 어떤 뮤텍스도 소유할 수 없으므로 대기상태가 된다는 것을 보여주기 위한 것이다.
Console.WriteLine("--------- " + Thread.CurrentThread.Name + " - releases gM1"); gM1.ReleaseMutex(); Thread.Sleep(2000);뮤텍스 gM1을 해제한다는 메시지를 콘솔에 출력하고 ReleaseMutex()를 호출하여 해제한다. 뮤텍스 gM1을 해제한 다음에 무슨 일이 일어나는지 알아보기 위해 2초동안 대기한다.
Console.WriteLine("--------- " + Thread.CurrentThread.Name + " - releases gM2"); gM2.ReleaseMutex(); Thread.Sleep(2000);마찬가지로 뮤텍스 gM2를 해제하고 무슨 일이 일어나는지 알아보기 위해 2초 동안 대기한다.
WaitHandle.WaitAll(IAutoResetEvent); Console.WriteLine("All threads just have finished"); }WaitHandle을 이용하여 모든 이벤트가 신호상태가 될 때 까지 기다린다. 실제로 모든 쓰레드가 종료되기 때문에 이 조건은 바로 만족되고 메시지가 콘솔에 출력된다.
public void DoSomething1() { Console.WriteLine("DoDomething1 started, Mutex.WaitAll(Mutex[])"); Mutex[] IMutex = new Mutex[2]; IMutex[0] = gM1; IMutex[1] = gM2; Mutex.WaitAll(IMutex); Console.WriteLine("DoSomething1 finished, Mutex.WaitAll(Mutex[])"); are1.Set(); }첫번째 쓰레드 thread1에서 실행하는 메소드 DoSomething1은 두 개의 뮤텍스를 소유할 때만 일을 처리한다. 결과화면에서 알 수 있는 것처럼 thread1이 제일 먼저 시작했지만 뮤텍스를 가장 마지막에 소유하므로 완료(finished) 메시지가 가장 마지막에 출력되는 것을 알 수 있다. 결과에서 알 수 있는 것처럼 WaitAll은 배열에 지정된 모든 요소에 대한 핸들을 가질 때 까지 대기한다. 마지막에 are1.Set()은 thread1의 작업이 끝났음을 알려주는 것이다. are1.Set()은 이벤트를 비신호상태에서 신호상태로 변경한다. 이벤트에 대해서 기억나지 않는다면 이전 글, 11. 이벤트를 참고하기 바란다.
public void DoSomething2() { Console.WriteLine("DoDomething2 started, gM1.WaitOne()"); gM1.WaitOne(); Console.WriteLine("DoSomething2 finished, gM1.WaitOne()"); are2.Set(); }thread2에서 실행하는 메소드 DoSomething1은 gM1.WaitOne()을 사용해서 뮤텍스 gM1을 이용할 수 있게 될 때 까지 기다린다. 결과화면에서 알 수 있는 것처럼 Main이 뮤텍스 gM1의 소유권을 해제한 다음에 가장 먼저 실행된다는 것을 알 수 있다. 마찬가지로 실행이 끝난 다음에는 이벤트 are2를 신호상태로 설정한다.
public void DoSomething3() { Console.WriteLine("DoDomething3 started, Mutex.WaitAny(Mutex[])"); Mutex[] IMutex = new Mutex[2]; IMutex[0] = gM1; IMutex[1] = gM2; Mutex.WaitAny(IMutex); Console.WriteLine("DoSomething3 finished, Mutex.WaitAny(Mutex[])"); are3.Set(); }thread3에서 실행하는 메소드 DoSomething3은 IMutex에 지정된 뮤텍스 gM1과 gM2를 기다린다. 여기서 WaitAny를 사용하였으므로 배열에 지정된 핸들 중에 먼저 사용할 수 있는 핸들이 있으면 그 핸들에 대한 소유권을 얻고 쓰레드의 대기 상태를 해제한다. 결과화면에서 알 수 있는 것처럼 Main에서 뮤텍스 gM1을 해제했을 때 gM1에 대한 소유권을 얻고 대기상태를 해제한다는 것을 알 수 있다.
public void DoSomething4() { Console.WriteLine("DoDomething4 started, gM2.WaitOne()"); gM2.WaitOne(); Console.WriteLine("DoSomething3 finished, gM2.WaitOne()"); are4.Set(); }thread4에서 실행하는 메소드 DoSomething4는 gM2.WaitOne()을 사용하는 것에서 알 수 있는 것처럼 뮤텍스 gM2를 얻을 수 있을 때 까지 대기한다. 결과화면에서 알 수 있는 것처럼 Main 쓰레드에서 뮤텍스 gM2의 소유권을 해제한 다음에 실행되는 것을 알 수 있다.
이름 : dining.cs using System; using System.Threading; namespace hanbit { class Table { static Mutex gM1 = new Mutex(false); static Mutex gM2 = new Mutex(false); static Mutex gM3 = new Mutex(false); static Mutex gM4 = new Mutex(false); static Mutex gM5 = new Mutex(false); static Mutex[] gFork = new Mutex[5]; static bool bContinue; public bool Continue { get { return bContinue; } set { bContinue = value; } } public void Stop() { bContinue = false; } public Table() { bContinue = true; gFork[0] = gM1; gFork[1] = gM2; gFork[2] = gM3; gFork[3] = gM4; gFork[4] = gM5; } public void GetForks(int threadID) { Mutex[] IFork = new Mutex[2]; IFork[0] = gFork[threadID]; IFork[1] = gFork[(threadID + 1) % 5]; WaitHandle.WaitAll(IFork); public void DropForks(int threadID) { gFork[threadID].ReleaseMutex(); gFork[(threadID + 1) % 5].ReleaseMutex(); } } class Philosopher { Random rand = new Random(DateTime.Now.Millisecond); private int ThreadID; private Table aTable; public Philosopher(int threadId, Table table) { this.ThreadID = threadId; this.aTable = table; } private void Think() { Console.WriteLine(Thread.CurrentThread.Name + " Thinking."); Thread.Sleep(rand.Next(1, 200)); } private void Eat() { Console.WriteLine(Thread.CurrentThread.Name + " Eating."); Thread.Sleep(rand.Next(1, 200)); } public void Philosophize() { while (aTable.Continue) { aTable.GetForks(ThreadID); //Eat this.Eat(); aTable.DropForks(ThreadID); //Think this.Think(); } } } ~Philosopher() { } } class AppMain { static void Main(string[] args) { Table table = new Table(); Philosopher[] IPhil = new Philosopher[5]; Thread[] IThread = new Thread[5]; for (int loopctr = 0; loopctr < 5; loopctr++) { IPhil[loopctr] = new Philosopher(loopctr, table); IThread[loopctr] = new Thread(new ThreadStart(IPhil[loopctr].Philosophize) ); IThread[loopctr].Name = "Philosopher " + loopctr; IThread[loopctr].Start(); } Thread.Sleep(5000); table.Stop(); Thread.Sleep(1000); Console.WriteLine("Primary Thread ended."); Console.WriteLine("Press any key to return."); Console.Read(); } } }예제를 컴파일하고 실행하면 결과는 다음과 같을 것이다.
소스 코드를 살펴보도록 하자
class Table { static Mutex gM1 = new Mutex(false); static Mutex gM2 = new Mutex(false); static Mutex gM3 = new Mutex(false); static Mutex gM4 = new Mutex(false); static Mutex gM5 = new Mutex(false); static Mutex[] gFork = new Mutex[5]; static bool bContinue; public bool Continue { get { return bContinue; } set { bContinue = value; } }Table 클래스를 선언하고, 5개의 뮤텍스를 생성한다. 생성하는 쓰레드에서 뮤텍스의 소유권을 갖도록 할 것이 아니므로 false를 사용하여 생성한다. gFork는 5개의 뮤텍스에 대한 컨테이너로 사용되고, 식사하는 철학자 문제에서 포크를 뜻한다. bContinue는 식사하는 철학자 프로그램을 계속 실행할지를 결정하기 위해 사용한다.
public void Stop() { bContinue = false; } public Table() { bContinue = true; gFork[0] = gM1; gFork[1] = gM2; gFork[2] = gM3; gFork[3] = gM4; gFork[4] = gM5; }Stop은 bContinue를 false로 설정하고, 식사하는 철학자 프로그램을 끝내기 위해 호출한다. Table 생성자에서는 bContinue = true로 설정하고, 각각의 포크를 컨테이너 gFork에 넣어둔다.
public void GetForks(int threadID) { Mutex[] IFork = new Mutex[2]; IFork[0] = gFork[threadID]; IFork[1] = gFork[(threadID + 1) % 5]; WaitHandle.WaitAll(IFork); } public void DropForks(int threadID) { gFork[threadID].ReleaseMutex(); gFork[(threadID + 1) % 5].ReleaseMutex(); }GetForks와 DropForks는 철학자가 테이블에서 포크를 갖는 것(뮤텍스에 대한 핸들을 얻는 것)과 포크를 테이블에 내려 놓는 것(뮤텍스에 소유권을 해제하는 것)을 나타낸 것이다. IFork[0]에는 철학자의 왼쪽에 있는 포크를, IFork[1]에는 철학자의 오른쪽에 있는 포크를 들도록 한 것이다. 마찬가지로 식사가 끝나면 철학자의 왼쪽에 있는 포크를 내려놓고, 그 다음에 오른쪽에 있는 포크를 내려놓도록 한 것이다.
class Philosopher { Random rand = new Random(DateTime.Now.Millisecond); private int ThreadID; private Table aTable; public Philosopher(int threadId, Table table) { this.ThreadID = threadId; this.aTable = table; }이것은 철학자 클래스이며, 식사하는 시간과 생각하는 시간을 임의의 숫자로 할당하기 위해 Random 클래스를 사용한다. 항상 다른 숫자를 얻기 위해 시스템 시간을 토대로하여 난수를 생성하도록 한다. 철학자 클래스 생성자는 쓰레드 ID를 갖고 있으며, 사용할 테이블을 지정하도록 하고 있다.
private void Think() { Console.WriteLine(Thread.CurrentThread.Name + " Thinking."); Thread.Sleep(rand.Next(1, 200)); } private void Eat() { Console.WriteLine(Thread.CurrentThread.Name + " Eating."); Thread.Sleep(rand.Next(1, 200)); }각각의 철학자는 생각하는 것과 식사하는 것을 하므로, 이를 묘사하는 두 개의 함수를 정의한다. 각각의 함수는 임의의 시간 동안 생각하고 식사할 수 있도록 하기 위해 1에서 200사이의 숫자만큼 대기하도록 한다.
public void Philosophize() { while (aTable.Continue) { aTable.GetForks(ThreadID); //Eat this.Eat(); aTable.DropForks(ThreadID); //Think this.Think(); } }실제로 철학자의 역할을 반복하는 함수다. Table.Continue가 true인 동안은 무한히 반복해서 실행한다. 포크를 들고 식사를 하고, 포크를 내려놓고 다시 식사하는 과정을 반복한다.
class AppMain { static void Main(string[] args) { Table table = new Table(); Philosopher[] IPhil = new Philosopher[5]; Thread[] IThread = new Thread[5]; for (int loopctr = 0; loopctr < 5; loopctr++) { IPhil[loopctr] = new Philosopher(loopctr, table); IThread[loopctr] = new Thread(new ThreadStart(IPhil[loopctr].Philosophize) ); IThread[loopctr].Name = "Philosopher " + loopctr; IThread[loopctr].Start(); } Thread.Sleep(5000); table.Stop(); Thread.Sleep(1000); Console.WriteLine("Primary Thread ended."); Console.WriteLine("Press any key to return."); Console.Read(); } }Main 클래스로 다섯 명의 철학자가 사용할 table을 만들고, 5명의 철학자를 IPhil [] 컨테이너에 할당한다.
public void GetForks(int threadID) { Mutex[] IFork = new Mutex[2]; IFork[0] = gFork[threadID]; IFork[1] = gFork[(threadID + 1) % 5]; WaitHandle.WaitAll(IFork); Console.WriteLine("Handle 1 - " + IFork[0].Handle + " Handle 2 - " + IFork[1].Handle ); }각각의 핸들을 화면에 출력하도록 해서 어떤 핸들을 갖고 있는지 확인하면 된다.
csc /debug:full dining.cs이와 같이 하면 디버깅 정보가 함께 생성된다.
b 56 go p vars p reg이와 같이 56번 라인에 중단점(break point)을 설정하고, 실행(go)하고, 변수 vars의 값을 출력하거나(p vars), 현재 프로세스의 값을 볼 수 있다(p). 현재 쓰레드에 대한 스택 상태를 보고 싶다면 reg를 사용하면 볼 수 있다. 주의할 것은 이러한 디버거를 이용하려면 반드시 /debug:full을 사용하여 디버깅해야한다.
화면에서 볼 수 있는 것처럼 p와 reg를 사용해서 자세한 명령을 볼 수 있으며, p위에는 현재 실행한 곳의 코드를 볼 수도 있다. (cordbg) 콘솔에서 ?를 입력하면 보다 자세한 명령을 볼 수 있다.
화면에서 볼 수 있는 거처럼 콘솔 응용 프로그램의 경우에 콘솔 창이 따로 나타나지만 모든 출력은 출력 창에 나타나는 것을 볼 수 있으며, 중단된 시점의 각 변수들의 값을 일목 요연하게 볼 수 있다는 것을 알 수 있다. 주의할 점이 있는데, 멀티 쓰레드 응용 프로그램의 경우에 중단점에서 디버거가 멈춰 있어도 계속해서 실행되기 때문에 잠시 머뭇거린 사이에 응용 프로그램이 종료된다. 따라서 Main에서 Thread.Sleep을 충분히 늘려놓고 사용법을 탐색해 보거나, 아니면 다른 응용 프로그램으로 먼저 연습해 보기 바란다. 지역 창에서 gM1 - gM5까지의 트리와 IFork의 트리를 충분히 펼쳐보면 각각의 핸들(Handle) 값을 볼 수 있으며, 현재 IFork에서 어떤 gM?에 해당하는 핸들을 갖고 있는지 알 수 있다. 또한 코드 창 위에 보면 실행중인 프로그램, 쓰레드, 스택 프레임이 무엇인지 알 수 있다. CLR 디버거는 CLR 환경에 대해서만 디버깅할 수 있는 단점은 있지만 닷넷 프레임워크만으로도 충분히 훌륭하게 디버깅할 수 있다는 것을 알 수 있다.
이전 글 : 객체지향 펄
최신 콘텐츠