프로세스와 스레드 #
- 프로세스(Process)
- 프로그램이 실행되어 메모리에 적재된 인스턴스이다.
- 스레드(Thread)
- 프로세스 내에서 실행되는 흐름의 단위이다.
- 운영체제가 CPU 시간을 할당하는 기본 단위이다.
- 멀티 스레드의 장점
- 응답성을 높일 수 있다.
- 자원 공유가 쉽다.
- 자원을 공유하므로 경제적이다.
- 멀티 스레드의 단점
- 구현이 까다롭다.
- 스레드를 너무 많이 사용하면 문맥교환이 잦아져서 오히려 성능이 더 저하된다.
스레드 시작하기 #
- .NET은 스레드를 제어하는
System.Threading.Thread
클래스를 제공한다.
Thread
의 인스턴스를 생성한다. 인수로 실행할 메소드를 넘긴다.
Thread.Start()
메소드를 호출해서 스레드를 시작한다.
Thread.Join()
메소드를 호출해서 스레드가 끝날 때까지 기다린다.
스레드 임의로 종료시키기 #
Thread.Abort()
메소드를 호출하면 즉시 종료되지는 않는다.
- CLR은 해당 스레드가 실행 중이던 코드에
ThreadAbortException
을 던진다. 이 때 예외를 catch
하는 코드가 있으면 처리한 다음 finally
블록까지 실행한 후에야 해당 스레드는 완전히 종료된다.
- 왠만하면 사용하지 않는 것이 좋다. 해당 스레드가 독점 자원을 잠근 후 종료되면, 그 자원은 해제되지 못하기 때문이다.
스레드의 상태 변화 #
- .NET은 스레드의 상태를
ThreadState
열거형에 정의해두었다.
상태 |
설명 |
Unstarted |
스레드 객체를 생성한 후 Thread.Start() 메소드가 호출되기 전의 상태이다. |
Running |
스레드가 시작하여 동작 중인 상태이다. Unstarted 에서 Thread.Start() 하면 이 상태가 된다. |
Suspended |
스레드의 일시 중단 상태이다. Thread.Suspend() 메소드를 통해 이 상태로 만들 수 있으며, Thread.Resume() 메소드를 통해 다시 Running 상태로 만들 수 있다. |
WaitSleepJoin |
스레드가 블록(Block)된 상태이다. Monitor.Enter() 나 Thread.Sleep() 나 Thread.Join() 메소드를 호출하면 이 상태가 된다. |
Aborted |
스레드가 취소된 상태이다. Thread.Abort() 메소드를 호출하면 이 상태가 된다. 이후 Stopped 상태로 전환되어 완전히 중지된다. |
Stopped |
스레드가 중지된 상태이다. Thread.Abort() 메소드를 호출하거나 실행 중인 메소드가 종료되면 이 상태가 된다. |
Background |
스레드가 백그라운드로 동작하고 있음을 나타낸다. 포어그라운드의 경우 스레드가 하나라도 살아 있으면 프로세스가 죽지 않는다. 반면 백그라운드의 경우 스레드가 살아 있는지와 관계없이 프로세스가 죽으면 스레드도 모두 죽는다. |
ThreadState
열거형은 Flags
애트리뷰트를 갖고 있다.
- 따라서 이 열거형의 요소들의 집합으로 표현할 수 있다. 즉, 비트 필드(Bit field)로 처리할 수 있다.
[Flags]
enum MyEnum
{
Apple = 1 << 0,
Orange = 1 << 1,
Kiwi = 1 << 2,
Mango = 1 << 3,
};
Console.WriteLine((MyEnum) 1); // Apple
Console.WriteLine((MyEnum) (1 | 4)); // Apple, Kiwi
// Flags 애트리뷰트를 갖고 있는 열거형은 요소의 집합으로 표현될 수 있다.
- 비트 연산을 통해
ThreadState
필드의 값을 확인할 수 있다.
상태 |
10진수 |
2진수 |
Running |
0 |
000000000 |
StopRequested |
1 |
000000001 |
SuspendRequested |
2 |
000000010 |
Background |
4 |
000000100 |
Unstarted |
8 |
000001000 |
Stopped |
16 |
000010000 |
WaitSleepJoin |
32 |
000100000 |
Suspended |
64 |
001000000 |
AbortRequested |
128 |
010000000 |
Aborted |
256 |
100000000 |
Thread th = new Thread(...);
if ((th.ThreadState & ThreadState.Aborted) == ThreadState.Aborted)
Console.WriteLine("스레드가 정지했습니다.");
else if ((th.ThreadState & ThreadState.Stopped) == ThreadState.Stopped)
Console.WriteLine("스레드가 취소되었습니다.");
인터럽트: 스레드를 임의로 종료하는 다른 방법 #
Thread.Interrupt()
메소드
Thread.Abort()
메소드는 강제로 종료하지만, Thread.Interrupt()
메소드는 스레드가 동작중인 상태(Running 상태)를 피해서 WaitJoinSleep상태에 들어갔을 때 ThreadInterruptedException
예외를 던져서 스레드를 중지시킨다.
- 절대로 중단되면 안 되는 작업을 하고 있을 때는 중단되지 않는다는 보장을 받을 수 있다.
스레드 간의 동기화 #
- 동기화(Synchronization)
- 스레드의 자원에 대한 접근을 제어하는 방법이다.
- 스레드 동기화에서 가장 중요한 것은 자원을 한 번에 하나의 스레드가 사용하도록 보장하는 것이다.
- .NET은
lock
키워드와 Monitor
클래스를 제공한다.
class Counter
{
const int LOOP_COUNT = 100;
private readonly object thisLock;
private int count;
public int Count { get => count; }
public Counter()
{
thisLock = new object();
count = 0;
}
public void Increase()
{
int loopCount = LOOP_COUNT;
while (loopCount-- > 0)
{
lock (thisLock) // 여기는 크리티컬 섹션이다
{
count++;
Console.WriteLine($"Increase: {count}");
}
Thread.Sleep(1);
}
}
public void Decrease()
{
int loopCount = LOOP_COUNT;
while (loopCount-- > 0)
{
lock (thisLock) // 여기는 크리티컬 섹션이다
{
count--;
Console.WriteLine($"Decrease: {count}");
}
Thread.Sleep(1);
}
}
}
class MainApp
{
static void Main(string[] args)
{
Counter counter = new Counter();
// 두 개의 스레드
Thread inc = new Thread(new ThreadStart(counter.Increase));
Thread dec = new Thread(new ThreadStart(counter.Decrease));
// 스레드 시작
inc.Start();
dec.Start();
// 스레드가 끝날 때까지 기다린다
inc.Join();
dec.Join();
Console.WriteLine($"Count: {counter.Count}"); // 0
}
}
Monitor
클래스로 동기화하기
lock
키워드로 똑같은 기능을 하는 Monitor.Enter()
(크리티컬 섹션 만들기)와 Monitor.Exit()
(크리티컬 섹션 제거하기)이 있다.
lock
키워드도 사실은 이 두 메서드들을 바탕으로 구현되어있다.
public void Increase()
{
int loopCount = LOOP_COUNT;
while (loopCount-- > 0)
{
Monitor.Enter(thisLock);
try
{
count++;
}
finally
{
Monitor.Exit(thisLock);
}
Thread.Sleep(1);
}
}
lock
보다 좀 더 섬세하게 멀티 스레드 간의 동기화를 가능하게 해주는 메서드들…
Monitor.Wait()
메서드
- 스레드를 WaitSleepJoin상태로 만든다. 해당 스레드는 갖고 있던 lock을 내려놓고 Waiting Queue에 입력된다.
Monitor.Pulse()
메서드
- CLR은 Waiting Queue에서 첫 번째 위치에 있는 스레드를 꺼내서 Ready Queue에 입력시킨다. 그 스레드는 차례로 lock을 얻어서 Running상태에 들어간다.
public void Increase()
{
int loopCount = LOOP_COUNT;
while (loopCount-- > 0)
{
lock (thisLock)
{
// count가 0보다 크거나
// 다른 스레드에 의해 lockedCount가 true가 되면
// Wait()으로 블록된다.
while (count > 0 || lockedCount == true)
Monitor.Wait(thisLock);
lockedCount = true; // 다른 스레드가 사용하지 못하도록한다.
count++;
lockedCount = false;
Monitor.Pulse(thisLock); // 다른 스레드를 깨운다.
}
Thread.Sleep(1);
}
}
Task와 Task 그리고 Parallel #
- 지금까지 보았던 멀티 스레드는…
- 여러 개의 작업을 각각 처리해야하는 상황이었다. (쪼갠 것 아님)
- 지금부터 볼 병렬 처리는…
- 하나의 작업을 쪼갠 뒤 쪼개진 작업들을 동시에 처리하는 것이다.
- 비동기(Asynchronous) 처리는…
- 메소드를 호출한 뒤에 메소드의 종료를 기다리지 않고 바로 다음 코드를 실행하는 것이다.
System.Threading.Tasks.Task 클래스 #
Task
클래스
- 비동기적으로 실행되는 단일 작업이다.
- 인스턴스를 생성할 때
Action
대리자를 넘겨받는다. 즉, 반환형을 갖지 않는 메소드, 익명 메소드, 무명 함수 등을 넘겨받는다.
// 실행할 작업물
Action action = () =>
{
Thread.Sleep(1000); // 이 코드에 상관없이 "동기"가 출력된다.
Console.WriteLine("비동기");
};
// Task로 할당
Task task = new Task(action);
// 비동기로 실행한다.
task.Start();
Console.WriteLine("동기");
// 비동기 코드가 완료될 때까지 대기한다.
task.Wait();
// 위와 같은 코드인데
// Task의 생성과 시작을 한번에 한다.
var task = Task.Run(() =>
{
Thread.Sleep(1000);
Console.WriteLine("비동기");
});
Console.WriteLine("동기");
task.Wait();
코드의 비동기 실행 결과를 주는 Task 클래스 #
Task<Result>
클래스
- 비동기 실행 결과를 손쉽게 얻을 수 있다.
- 인스턴스를 생성할 때
Func
대리자를 넘겨받아서 결과를 반환한다.
var task = Task<List<int>>.Run(() =>
{
Thread.Sleep(1000);
List<int> list = new List<int>();
list.Add(3);
list.Add(4);
list.Add(5);
return list;
});
task.Wait();
List<int> total = new List<int>();
total.Add(0);
total.Add(1);
total.Add(2);
total.AddRange(task.Result.ToArray()); // 0, 1, 2, 3, 4, 5
손쉬운 병렬 처리를 가능케 하는 Parallel 클래스 #
System.Threading.Tasks.Parallel
클래스
For()
, Foreach()
등의 메소드로 병렬 처리를 좀 더 쉽게 구현할 수 있다.
- 해당 메소드를 병렬로 호출할 때 몇 개의 스레드를 사용할지는
Parallel
클래스가 내부적으로 판단하여 최적화한다.
List<int> total = new List<int>();
// 0부터 100사이의 정수를 메소드의 인수로 넘긴다.
// 병렬로 처리된다.
Parallel.For(0, 100, (int i) =>
{
if (i % 2 == 0)
lock (total)
total.Add(i);
});
for (int i = 0; i < total.Count; i++)
Console.Write($"{total[i]} ");
async 한정자와 await 연산자로 만드는 비동기 코드 #
-
async
한정자
- 컴파일러에게 해당 메서드가 비동기 작업이 포함된
await
를 가지고 있음을 알려준다.
- 내부에서
await
연산자를 찾으면 그곳에서 호출자에게 제어를 돌려준다.
- 내부에서
await
연산자를 만나지 못하면 제어를 돌려주지 않으므로, 그냥 동기적으로 실행하게 된다.
- 반환 형식
void
: 실행하고 잊어버릴 작업을 수행하는 메소드일 때. 리턴형이 없으므로 비동기 메서드를 호출하는 쪽에서 비동기 제어를 할 수 없게 된다.
Task
: 리턴값이 없는 경우.
Task<Result>
: 리턴값이 있는 경우.
-
await
연산자
- 단항 연산자이며
awaitable
클래스(GetAwaiter()
메서드를 갖는다)를 인수로 갖는다.
- 이것은
Task
혹은 Task<T>
가 일반적이다.
- 이 작업을 하는 동안 스레드가 차단되지 않도록(다른 일도 할 수 있도록)한다. 그리고 그 결과를 사용하기 전에 해당
Task
가 끝날 때까지 기다린다. 완료 되면 다음 실행문부터 실행을 계속한다.
// void
public async void AsyncFunc()
{ }
// Task
public async Task AsyncFunc()
{
await Task.Delay(1000);
}
// Task<TResult>
public async Task<int> AsyncFunc()
{
await Task.Delay(1000);
return 1;
}
public static void Main(string[] args)
{
TaskTest();
System.Console.WriteLine("Main Thread is NOT Blocked");
Console.ReadLine();
}
private static async void TaskTest()
{
await Task.Delay(5000); // 5초를 기다린다.
System.Console.WriteLine("TaskTest Done");
}
// async 한정자가 있으므로 await를 가질 수 있다.
public static async Task Main(string[] args)
{
Task t = TaskTest1();
System.Console.WriteLine("Do Something Before TaskTest");
await t; // 여기서 t가 끝날 때까지 기다린다.
System.Console.WriteLine("Do Something after TaskTest");
Console.ReadLine();
}
private static async Task TaskTest1()
{
await Task.Delay(5000); // 5초를 기다린다.
System.Console.WriteLine("TaskTest Done");
}
Task<TResult>
를 반환하는 async
메서드
public static async Task Main(string[] args)
{
Task<int> t = TaskTest();
for(int i = 0; i < 10; i++)
{
System.Console.WriteLine("Do Something Before TaskTest");
}
int UID = await t; // await을 통해서 반환값을 추출할 수 있다.
Console.WriteLine($"UserID : {UID}");
Console.ReadLine();
}
private static async Task<int> TaskTest()
{
int UID = await ...; // DB or server에서 UID 얻어오는 비동기 메서드 호출
System.Console.WriteLine("TaskTest Done");
return UID;
}
async static private void AsyncMethod(int count)
{
Console.WriteLine("B");
// 호출자에게 제어를 돌려주고 아래 코드는 비동기적으로 처리된다.
// 아래 Task가 끝나야지 다음 문장이 실행 된다.
await Task.Run(async () =>
{
for (int i = 0; i < count; i++)
{
Console.WriteLine($"{i} / {count} ...");
await Task.Delay(100); // Thread.Sleep()의 비동기 버전이다.
}
});
Console.WriteLine("D");
}
static void Caller()
{
Console.WriteLine("A");
AsyncMethod(3);
Console.WriteLine("C");
}
static void Main(string[] args)
{
Caller();
Console.ReadLine(); // (프로그램 종료 방지)
}
A
B
C
0 / 3 ...
1 / 3 ...
2 / 3 ...
D