[이것이 C#이다] 19장 스레드와 태스크
👉 프로세스(Process)
란 실행 파일이 실행되어 메모리에 적재된 인스턴스
👉 스레드(Thread)
란 운영체제가 CPU 시간을 할당하는 기본 단위
👍 멀티 스레드 활용 시 장점
👎 멀티 스레드 활용 시 단점
작업 간 전환(Context Switching)
비용 증가System.Threading.Thread
🔗 Thread 인스턴스 생성 및 실행 예
// 스레드 실행 대상 메소드
static void DoSomething()
{
for(int i=0;i<5;i++)
{
Console.WriteLine("DoSomething : {0}", i);
// 인수(밀리초) 만큼 CPU 사용을 멈춤
Thread.Sleep(10:
}
}
static void Main(string[] args)
{
// Thread의 인스턴스 생성
Thread t1 = new Thread(new ThreadStart(DoSomething);
// 스레드 시작
t1.Start();
// 스레드의 종료 대기
t1.Join();
}
.NET 프레임워크에서만 지원하는
Thread.Abort()
메소드
💡 Abort()
메소드는 호출과 동시에 스레드를 즉시 종료하지는 않음
ThreadAbortException
발생👉 도중에 강제로 중단되더라도 프로세스 자신이나 시스템에 영향을 받지 않는 작업에 한해서만 사용하는 것이 좋음
🔗 Thread 임의 종료 예
// 스레드 실행 대상 메소드
static void DoSomething()
{
try
{
for(int i=0;i<5;i++)
{
Console.WriteLine("DoSomething : {0}", i);
Thread.Sleep(10:
}
}catch(ThreadAbortedException){ ... }
finally{ ... }
}
static void Main(string[] args)
{
Thread t1 = new Thread(new ThreadStart(DoSomething);
t1.Start();
// 스레드 취소(종료)
t1.Abort();
t1.Join();
}
👉 ThreadState 열거형
에 정의
Flag
애트리뷰트 : 자신이 수식하는 열거형을 비트 필드로 처리할 수 있음상태 변화 규칙
🔗 Flag 활용 예시
[Flags]
enum MyEnum{
Apple = 1 << 0, // 1(0001)
Orange = 1 << 1, // 2(0010)
}
Console.WriteLine((MyEnum)1); // Apple
Console.WriteLine((MyEnum)2); // Orange
Console.WriteLine((MyEnum)(1 | 2)); // Apple, Orange (0011)
💡 ThreadState 필드를 통해 상태를 확인할 때는 반드시 비트 연산을 이용
if(t1.ThreadState & ThreadState.Aborted == ThreadState.Aborted)
Console.WriteLine("스레드가 정지했습니다.");
else if(t1.ThreadState & ThreadState.Stopped == ThreadState.Stopped)
Console.WriteLine("스레드가 취소됐습니다.");
스레드를 안전하게 임의로 종료하기 위한
Thread.Interrupt()
메소드
💡 Interrupt()
메소드는 스레드가 한참 동작 중인 상태를 피해서 스레드를 중지시킴
Running
상태를 피해 WaitJoinSleep
상태에 들어갔을 때 WaitJoinSleep
상태라면 즉시ThreadInterruptedException
예외를 던짐👉 절대로 중단되면 안 되는 작업의 안전성 보장 가능
🔗 인터럽트 임의 종료 예
static void Main(string[] args)
{
Thread t1 = new Thread(new ThreadStart(DoSomething);
t1.Start();
// 스레드 취소(종료)
t1.Interrupt();
t1.Join();
}
동기화(Synchronization)
란 스레드들이 순서를 갖춰 자원을 사용하게 하여 질서를 정해주는 것
👉 자원을 한 번에 하나의 스레드가 사용하도록 보장하는 것이 사명
임계 영역(critical section)
: 한 번에 한 스레드만 사용할 수 있는 코드 영역
🔗 lock 키워드 사용 예
class Counter
{
private readonly object thisLock = new object();
public void Increase()
{
lock(thisLock)
{
count = count + 1;
}
}
}
MyClass obj = new MyCLass();
Thread t1 = new Thread(new ThreadStart(obj.Increase);
Thread t2 = new Thread(new ThreadStart(obj.Increase);
t1.Start();
t2.Start();
t1.Join();
t2.Join();
// 2가 보장됨
Console.WriteLine(obj.count);
⭐️ lock은 다른 스레드들이 대기하게 되므로, 스레드의 동기화를 설계할 때는 크리티컬 섹션을 반드시 필요한 곳에만 사용하여 성능 저하를 최소화해야함
💡 lock 키워드의 매개변수는 참조형이면 모두 가능
But 외부 코드에서도 접근할 수 있는 변수는 사용 금지
Monitor
클래스
Monitor
클래스는 스레드 동기화에 사용하는 몇 가지 정적 메소드를 제공
💡 lock 키워드는 Monitor
클래스의 Enter()
& Exit()
메소드를 바탕으로 구현
따라서 다음 두 코드는 동일한 역할
public void Increase()
{
lock(thisLock)
{
count = count + 1;
}
}
public void Increase()
{
Monitor.Enter(thisLock);
try
{
count = count + 1;
}
finally
{
Monitor.Exit(thisLock);
}
}
⭐️ Monitor.Wait()
& Monitor.Pulse()
메소드를 활용한 섬세한 동기화 제어 (저수준 동기화)
Monitor.Wait()
: 스레드를 WaitSleepJoin
상태로 만듦
Waiting Queue
에 들어감Monitor.Pulse()
: Wait()
메소드를 바로 깨워주는 역할
Waiting Queue
의 처음 스레드를 Ready Queue
에 입력Running
상태로 들어감멀티 스레드 애플리케션의 성능 향상에 사용됨
🔗 Wait & Pulse 사용 예
bool lockedCount = false;
lock(thisLock)
{
// 다른 스레드가 작업중이므로 대기
while(count > 0 || lockedCount == true)
Monitor.Wait(thisLock);
lockedCount = true;
count++;
lockdCount = false;
// 다른 Wait 스레드를 깨워줌
Monitor.Pulse(thisLock);
}
병렬 처리 : 하나의 작업을 여러 작업자가 나눠서 수행한 뒤 다시 하나의 결과로 만드는 것
비동기 처리 : 작업 A를 시작하고 마냥 대기하는 대신 곧이어 다른 작업 B,C..를 수행하다가 A가 끝나면 결과를 받아내는 처리 방식
동기 처리 : 하나의 작업이 끝날 때까지 대기
System.Threading.Tasks
네임스페이스는 병행성 코드나 비동기 코드 활용을 돕는 여러 클래스 제공
System.Threading.Tasks.Task
클래스Task 클래스
: 인스턴스를 생성할 때 Action 대리자
를 넘겨 받아 비동기 호출
🔗 Task 인스턴스 생성 및 실행 예
Action someAction = () =>
{
Thread.Sleep(1000);
Console.WriteLine("Printed asynchronously.");
};
Task myTask = new Task(someAction);
// 생성자에서 넘겨받은 무명 함수 비동기 호출
myTast.Start();
...or...
// Task의 생성과 시작을 한꺼번에 처리
var myTask = Task.Run( () =>
{
Thread.Sleep(1000);
Console.WriteLine("Printed asynchronously.");
}
};
Console.WriteLine("Printed synchronously.");
// 비동기 호출이 완료될 때까지 대기
myTask.Wait();
실행 결과
Printed synchronously.
Printed asynchronously.
👉 Task 클래스는 동기 실행을 위한RunSynchronously()
메소드도 제공
Task<TResult>
Task<TResult>
: 인스턴스를 생성할 때 Func 대리자
를 넘겨 받아 비동기 호출 후 결과 반환
Task.Result
로 반환🔗 Task<TResult>
인스턴스 생성 및 실행 예
var myTask = Task<List<int>>.Run( () =>
{
Thread.Sleep(1000);
List<int> list = new List<int>();
list.Add(2);
list.Add(3);
return list;
}
);
var myList = new List<int>();
myList.Add(0);
myList.Add(1);
myTask.Wait();
myList.AddRange(myTask.Result.ToArray());
// 리스트 요소 - 0,1,2,3
Parallel
클래스System.Threading.Tasks.Parallel
: For(), Foreach() 등의 메소드를 제공하여 병렬 처리를 쉽게 도와줌
💡 병렬 처리에 몇 개의 스레드를 사용할 지는 내부적으로 판단하여 최적화함
🔗 사용 예
Parallel.For(from, to, (long i) =>
{
if(IsPrime(i))
lock(total)
total.Add(i);
});
Parallel.ForEach(total, prime =>
{
Console.WriteLine("{0} is prime",prime);
});
👉 async 한정자
는 메소드, 이벤트 처리기, 태스크, 람다식 등을 수식하여 C# 컴파일러가 해당 호출 코드를 만날 때 결과를 기다리지 않고 바로 다음 코드로 이동하도록 처리
💡 반환 형식은 반드시 다음 세가지 중 하나
Task<TResult>
⭐️ 기본 형식
public static async Task MyMethodAsync()
{
...
}
async 한정
시await 연산자
를 만나는 곳에서 호출자에게 제어를 돌려주며,await 연산자
가 없는 경우 동기로 실행됨
다음 그림과 같이 a와 b의 흐름을 동시에 실행하게 됨
Task.Delay()
메소드 : Thread.Sleep()
의 비동기 버전
👉 인수로 입력받은 시간이 지나면 Task 객체를 반환
.NET 클래스 라이브러리에서
~Async()
양식의 메소드들
💡 async로 한정한 코드를 호출하는 코드도 역시 async로 한정되어 있어야 함
🔗 ReadAsync
& WriteAsync
사용 예
async Task<long> CopyAsync(string fromPath, string ToPath)
{
using(var fromStream = new FileStream(FromPath, FileMode.Open))
{
long totalCopied = 0;
using(var toStream = new FileStream(ToPath, FileMode.Create))
{
byte[] buffer = new byte[1024];
int nRead = 0;
while((nRead = await fromStream.ReadAsync(buffer,0,buffer.Length)) != 0)
{
await toStream.WriteAsync(buffer, 0, nRead);
totalCopied += nRead;
}
}
return totalCopied;
}
}
long result = await CopyAsync(FromPath, ToPath);