Threading
Thread는 Process 내부에서 생성되는 명령어를 실행하기 위한 스케줄링의 단위이다. Process를 생성할 때 기본적으로 한 개의 Main Thread를 생성한다. Muti-Core CPU에서는 여러 개의 Thread를 동시에 실행할 수 있어서 이를 활용한 응용 프로그램을 만들 수 있다.
System.Threading
Thread
Thread를 생성하고 실행하는 데 필요한 명령어를 가진 클래스다.
Foreground Thread
Foreground Thread는 Main Thread의 종료와 상관없이 모든 Thread가 종료되어야 Process가 종료된다.
static void Main()
{
Console.WriteLine("Main Thread Start");
Thread thread = new Thread(threadFunc);
thread.Start();
Console.WriteLine("Main Thread End");
}
static void threadFunc()
{
Console.WriteLine("Foreground Thread Start");
Thread.Sleep(3000); // 3초 동안 실행 중지
Console.WriteLine("Foreground Thread End");
}
실행 결과 :
Main Thread Start
Foreground Thread Start
Main Thread End
Foreground Thread End
Background Thread
Background Thread는 Thread의 종료 여부와 상관없이 Main Thread가 종료되면 Process도 종료된다. 단, Join()을 사용하면 해당 Thread가 종료할 때까지 현재 Thread는 대기한다.
static void Main()
{
Console.WriteLine("Main Thread Start");
Thread thread = new Thread(threadFunc);
thread.IsBackground = true;
thread.Start();
thread.Join(); // thread가 종료할 때까지 대기
Console.WriteLine("Main Thread End");
}
static void threadFunc()
{
Console.WriteLine("Background Thread Start");
Thread.Sleep(3000); // 3초 동안 실행 중지
Console.WriteLine("Background Thread End");
}
실행 결과 :
Main Thread Start
Background Thread Start
Background Thread End
Main Thread End
Monitor
Monitor는 임의의 객체를 잠금으로써 특정 코드의 접근을 막는 기능을 가지고 있다. 여러 Thread가 하나의 공유자원을 동시에 사용하면 문제가 발생하는데 Monitor를 통해 하나의 Thread에서만 공유자원을 사용할 수 있도록 함으로써 Thread를 동기화할 수 있다.
static void Main()
{
MyData data = new MyData(); // MyData 클래스 생략
Thread thread1 = new Thread();
Thread thread2 = new Thread();
thread1.Start(data);
thread2.Start(data);
thread1.Join();
thread2.Join();
Console.WriteLine(data.Number);
}
static void threadFunc(object inst)
{
Mydata data = inst as MyData;
for (int i = 0; i < 100000; i++)
{
Monitor.Enter(data);
try
{
data.compute(); // MyData의 Method
}
finally
{
Monitor.Exit(data);
}
}
}
-
lock
키워드를 사용하면 try~finally와 Monitor의 역할을 대신 할 수 있다.
static void threadFunc(object inst)
{
Mydata data = inst as MyData;
for (int i = 0; i < 100000; i++)
{
lock (data)
{
data.compute();
}
}
}
Interlocked
Interlocked는 다중 Thread의 공유자원에 대한 Atomic Operation를 제공한다. 특히 64bit 변수의 연산 과정에서 동기화 문제가 생길 수 있는데 Atomic Operation을 통해 하나의 Thread에서 한 번에 처리할 수 있다. 이 경우에 Monitor을 사용하여 Thread를 동기화할 수 있지만, Interlocked에서 제공하는 연산 기능으로 간단하게 구현할 수 있다.
static void threadFunc(object inst)
{
long number = 0; // 8byte(64bit)
Interlocked.Exchange(ref number, 3); // number = 5;
Interlocked.Add(ref number, 5); // number += 5;
Interlocked.Increment(ref number); // number++;
Interlocked.Decrement(ref number); // number--;
}
ThreadPool
ThreadPool은 필요할 때마다 Thread를 꺼내 쓰고 다시 반환할 수 있는 기능을 제공한다. Thread를 따로 생성할 필요 없이 QueueUserWorkItem()
에게 Thread가 자동으로 생성되고 지정한 Method가 할당되어 실행한다.
static void Main()
{
Mydata data = new Mydata();
ThreadPool.QueueUserWorkItem(threadFunc, data);
ThreadPool.QueueUserWorkItem(threadFunc, data);
Thread.Sleep(3000); // 3초 동안 실행 중지
Console.WriteLine(data.Number);
}
EventWaitHandle
EventWaitHandle은 Signal과 Non-Signal의 상태 변화를 통해 Thread를 동기화하는 방법이다. WaitOn()
에서 Non-Signal이 Signal 상태로 바뀔 때까지 Thread가 더 실행되지 못하게 하고 대기 상태로 만든다. 앞서 나온 Background Thread의 Join()
과 유사한 역할을 한다.
static void Main()
{
Console.WriteLine("Main Thread Start");
EventWaitHandle ewh = new EventWaitHandle(false, EventResetMode.ManualReset); // Non-Signal 상태, 수동 Reset
Thread thread = new Thread(threadFunc);
thread.IsBackground = true;
thread.Start(ewh);
ewh.WaitOn(); // Signal 상태로 바뀔 때까지 대기
Console.WriteLine("Main Thread End");
}
static void threadFunc(object state)
{
EventWaitHandle ewh = state as EventWaitHandle;
Console.WriteLine("Background Thread Start");
Thread.Sleep(3000); // 3초 동안 실행 중지
Console.WriteLine("Background Thread End");
ewh.Set(); // Signal 상태로 전환
}
실행 결과 :
Main Thread Start
Background Thread Start
Background Thread End
Main Thread End
Acync
Async Method
acync
와 await
키워드를 사용하여 간편하게 비동기 호출을 할 수 있다. async
는 내부에 await
를 사용했으므로 해당 Method를 Async Method로 정의해주는 역할을 하고, await
는 대기 작업이 완료될 때까지 Method의 실행을 일시 중단하여 비동기를 적용한다.
async void AwaitDownloadString()
{
WebClient webClient = new WebClient();
string text = await webClient.DownloadStringTaskAsync("http://www.novonetworks.com/");
Console.WriteLine(text);
}
Task
Task는 Thread Pool에서 Thread를 가져와 비동기 작업을 실행한다. Task 객체를 만들기 위해서 생성자에 Method Delegate를 지정한다.
반환 값이 없는 Task
Task task = new Tack(() => { Thread.Sleep(3000); });
task.Start();
- Task를 생성할 필요 없이 Action Delegate를 전달하고 바로 작업을 시작할 수 있다.
Task task = Task.Factory.StartNew(() => { Thread.Sleep(3000); });
반환 값이 있는 Task
Task<int> task = new Task<int>(() =>
{
Thread.Sleep(3000);
return 3;
});
task.Start();
Console.WriteLine(task.Result); // 출력 결과 : 3
- 마찬가지로 **Task**는 Func Delegate를 전달하고 바로 작업을 시작할 수 있다.
Task<int> task = Task.Factory.StartNew<int>(() =>
{
Thread.Sleep(3000);
return 3;
});
Console.WriteLine(task.Result); // 출력 결과 : 3
Async Method가 아닌 경우의 비동기 처리
Async Method가 아닌 Method의 반환 형식을 **Task**로 바꾸면 await
를 이용해 비동기 호출을 적용할 수 있다.
async void AwaitDownloadString()
{
string text = await MyDownloadString("http://www.novonetworks.com/");
Console.WriteLine(text);
}
Task<string> MyDownloadString(string uri)
{
WebClient webClient = new WebClient();
return Task.Factory.StartNew(() =>
{
return webClient.DownloadString(uri);
});
}
비동기 호출의 병렬 처리
async
와 await
키워드를 사용해 비동기 호출을 적용하면 여러 작업을 병렬로 처리할 수 있다.
static void Main()
{
DoAsyncTask();
Console.WriteLine("Main Thread End");
}
static async void DoAsyncTask()
{
Stopwatch stopWatch = new Stopwatch();
var task1 = ThreeSeconds();
var task2 = FiveSeconds();
stopWatch.Start();
await Task.WhenAll(task1, task2);
stopWatch.Stop();
TimeSpan timeSpan = stopWatch.Elapsed;
Console.WriteLine("Total : " + (task1.Result + task2.Result));
Console.WriteLine("Result : " + timeSpan.Seconds);
}
static Task<int> ThreeSeconds()
{
return Task.Factory.StartNew(() =>
{
Thread.Sleep(3000);
return 3;
});
}
static Task<int> FiveSeconds()
{
return Task.Factory.StartNew(() =>
{
Thread.Sleep(5000);
return 5;
});
}
출력 결과 :
Main Thread End
Total : 8
Result : 5