대부분의 경우에, 언어나 플랫폼에 상관없이 쓰레드의 동작 방식을 이해하고 이를 활용하여 멀티쓰레드 애플리케이션을 구현할 수 있을 때 그 사람을 중급 이상의 개발자로 보게된다. 닷넷 혹은 자바와 같이 managed 환경하의 개발 방식을 지원하는 경우 쓰레드를 생성하여 활용하는 것이 생각보다 쉽다. 다만 어디까지 활용할 것인가는 개발자 선택의 몫이다. 쓰레드를 논할 때 간과하는 것이 소위 말하는 green thread vs. native thread이다. OS 레벨에서 나름대로의 방식으로 생성하여 관리하는 native thread와, managed 환경 즉, CLR이나 JVM 레벨에서 제공하는 green thread가 있다. 당연하게도 green thread가 결국은 OS 레벨에서 process로 매핑이 되건 native thread로 매핑이 되어 실행된다. 하지만, CLR의 green thread와 OS의 native thread가 일대일 매핑될 것이라는 가정은 금물이다. 하나의 OS native thread가 여러 애플리케이션의 여러 green thread를 동시에 처리할 수도 있기 때문이다. (닷넷은 덜 하겠지만, 자바의 경우 한때는 Windows, Unix, Linux에서 green thread가 다르게 표현되기도 했다. Windows에서는 native thread와 매핑되고, Linux에서는 한 때 process로 매핑되어 애플리케이션에서 쓰레드 생성마다 java process가 지속적으로 늘어나기도 했고, Solaris의 경우 lightweight process내의 user thread에 매핑되기도 했다.) 어쨌거나 디버깅 필요에 의해서 native thread 레벨까지 뒤져보는 것은 필요하나 일반적으로 green thread와 native thread 사이의 연관 관계에 대해서 일체의 가정을 두고 애플리케이션을 작성하는 것은 위험하다. 왜냐하면 CLR의 green thread 관련된 구현이 바뀔 경우, 애플리케이션이 기반한 가정이 치명상을 입을 수 있기 때문이다.
쓰레드 생성
쓰레드의 생성은 sealed 클래스라서 상속이 불가능한 System.Thread 클래스와 ThreadStart delegate을 이용한다.
new Thread(new ThreadStart(name_of_method)).Start(); // C# with delegate
new Thread(o => name_of_method(x)); // C# with lambda operator
ThreadStart delegate의 signature를 보면 public delegate void ThreadStart()로 되어 있어서 리턴값과 파라메터가 없는 메소드를 받을 수 있다. 이는 흡사 자바의 Runnable 인터페이스에서 public void run()을 구현하게 하는 것과 유사하다.
new Thread(new Runnable{ public void run() { ..... }} ).start(); // java.. 자바에는 delegate도, anonymous method도 없어서 anonymous class를 이용해야 한다.
쓰레드가 필요시마다 위에서 처럼 생성하게 되면 오버헤드가 발생하므로 대부분의 경우 System.Threading.ThreadPool 클래스의 static 메소드인 QueueUserWorkItem()을 이용한다.
ThreadPool.QueueUserWorkItem(o => name_of_method (x)); // C# with lambda operator
쓰레드 라이프싸이클
쓰레드는 다음과 같은 life cycle을 가진다.
주의할 점은, 한번 일을 끝낸 쓰레드 (그림에서 Finished 혹은 Aborted 상태)는 다시 활성화되지 않는다. Running 상태에 있는 쓰레드는 그림에서 보듯이 현 쓰레드를 잠시 쉬게 하는 Thread.Sleep(), 다른 쓰레드가 끝날 때까지 기다리겠다는 otherThread.Join(), 동기화 필요에 의한 Monitor.Wait() 등에 의해 WaitSleepJoin 상태로 가게되며 이 상태에서는 interruption을 받을 수 있다. 즉, 쉬는 상태에서 interruption을 받아 ThreadInterruptionException을 발생시키고 이를 처리하면서 Running 상태로 복귀할 수 있다. 또한 Thread.Abort()을 이용하여 쓰레드를 종료시킬 수 있는데, 이 메소드를 받은 쓰레드는 그림에서 처럼 ThreadAbortException을 발생시키며 AbortRequested 상태에 빠지게 된다. 재밌는 것은 이 상태에서 ResetAbort()를 이용하여 Abort() 명령을 무력화시키고 다시 Running 상태로 갈 수도 있다는 것이다.
Foreground thread vs. Background thread
쓰레드에는 foreground thread와 background thread가 있다. 이 둘은 모든 면에서 동일하나, 다른 점은 한 프로세스상의 모든 foreground thread가 완료되면 해당 프로세스는 종료되는 반면, background thread는 이런 기능이 없어서 foreground thread가 완료되어 프로세스가 끝나면 background thread는 실행 도중에 종료된다. 따라서 background thread가 데이터베이스나 네트웍 연결을 담당하고 있다면 도중 종료로 인해 해당 자원이 불안정한 상태에 빠질 수 있다. C#에서 Main() 쓰레드는 해당 프로세스 내의 하나의 쓰레드일 뿐이라서 Main() 쓰레드가 종료되었지만 다른 foreground thread가 살아 있다면 해당 프로세스는 다른 모든 foreground thread가 종료될 때까지 종료되지 않는다. 물론 다른 쓰레드가 모두 background thread라면 Main() 쓰레드 종료와 더불어 프로세스가 종료된다. background thread는 Thread.IsBackground 라는 property를 true로 설정함으로 적용할 수 있다.
Thread별 변수 저장
멀티 쓰레드 애플리케이션을 작성시 가끔 고민하는 부분이, 다수의 쓰레드가 하나의 인스턴스에 접근할 경우 각 쓰레드별 고유한 값을 저장할 수 있는 변수의 필요성을 느끼게 된다. 이 경우 사용할 수 있는 장치가 ThreadStatic attribute이다. System.ThreadStaticAttribute로 정의되어 있으며, 작성하고자 하는 클래스의 static 멤버에 이 속성을 추가함으로써 해당 static 멤버는 접근하는 모든 쓰레드에 대해 고유한 변수를 갖게 된다. static 멤버 변수는 해당 클래스의 모든 인스턴스 사이에 공유되는 값인데 반하여, ThreadStatic static 멤버 변수는 해당 클래스 및 인스턴스에 접근하는 모든 쓰레드에 고유한 값을 저장하는 변수가 되는 것이다.
아래는, ThreadStatic으로 지정된 Hey에 대해, thread1과 thread2는 같은 인스턴스에 대해 메소드를 호출하고, thread3는 클래스 메소드(static method)를 호출할 경우 ThreadStatic 변수인 static string Hey에 저장된 값이 무엇인지를 출력하는 간단한 프로그램이다.
using System;
using System.Threading;
public class ThreadStaticTest
{
public static void Main(string[] args)
{
MyThreadField a = new MyThreadField();
Thread thread1 = new Thread(new ThreadStart(a.SayHello1));
thread1.Start();
Thread thread2 = new Thread(new ThreadStart(a.SayHello1));
thread2.Start();
Thread thread3 = new Thread(new ThreadStart(MyThreadField.SayHello2));
thread3.Start();
}
}
public class MyThreadField
{
[ThreadStatic]
public static string Hey = "babo";
public void SayHello1()
{
for (int i = 0; i < 10; i++)
{
Hey = Hey + Thread.CurrentThread.GetHashCode();
Console.WriteLine("Thread {0} starting... {1}", Thread.CurrentThread.GetHashCode(), Hey);
Thread.Sleep(100);
}
}
public static void SayHello2()
{
for (int i = 0; i < 10; i++)
{
Hey = Hey + Thread.CurrentThread.GetHashCode();
Console.WriteLine("Thread {0} starting... {1}", Thread.CurrentThread.GetHashCode(), Hey);
Thread.Sleep(100);
}
}
}
결과는 예상대로 쓰레드 간에 간섭없이 Hey 값이 쓰레드 별로 해당 쓰레드에 의해서만 변경되고 있다.
Thread 3 starting... 3
Thread 4 starting... 4
Thread 5 starting... 5
Thread 5 starting... 55
Thread 4 starting... 44
Thread 3 starting... 33
Thread 5 starting... 555
Thread 3 starting... 333
Thread 4 starting... 444
Thread 5 starting... 5555
Thread 3 starting... 3333
Thread 4 starting... 4444
Thread 3 starting... 33333
Thread 4 starting... 44444
Thread 5 starting... 55555
Thread 3 starting... 333333
Thread 5 starting... 555555
Thread 4 starting... 444444
Thread 4 starting... 4444444
Thread 5 starting... 5555555
Thread 3 starting... 3333333
Thread 3 starting... 33333333
Thread 4 starting... 44444444
Thread 5 starting... 55555555
Thread 3 starting... 333333333
Thread 5 starting... 555555555
Thread 4 starting... 444444444
Thread 4 starting... 4444444444
Thread 5 starting... 5555555555
Thread 3 starting... 3333333333
ThreadStatic 속성을 제거하면 아래처럼 쓰레드간 간섭에 의해 Hey에 저장되는 값이 불규칙하게 증가하는 것을 알 수 있다.
Thread 3 starting... babo3
Thread 4 starting... babo34
Thread 5 starting... babo345
Thread 3 starting... babo3453
Thread 4 starting... babo34534
Thread 5 starting... babo345345
Thread 4 starting... babo3453454
Thread 3 starting... babo34534543
Thread 5 starting... babo345345435
Thread 4 starting... babo3453454354
Thread 3 starting... babo34534543543
Thread 5 starting... babo345345435435
Thread 4 starting... babo3453454354354
Thread 3 starting... babo34534543543543
Thread 5 starting... babo345345435435435
Thread 4 starting... babo3453454354354354
Thread 3 starting... babo3453454354354354
Thread 5 starting... babo34534543543543545
Thread 4 starting... babo345345435435435454
Thread 3 starting... babo345345435435435453
Thread 5 starting... babo3453454354354354535
Thread 3 starting... babo34534543543543545353
Thread 4 starting... babo345345435435435453534
Thread 5 starting... babo3453454354354354535345
Thread 4 starting... babo34534543543543545353454
Thread 3 starting... babo345345435435435453534543
Thread 5 starting... babo3453454354354354535345435
Thread 4 starting... babo34534543543543545353454354
Thread 3 starting... babo34534543543543545353454353
Thread 5 starting... babo345345435435435453534543535
댓글을 달아 주세요