Chapter19. 스레드와 테스크
1. 프로세스와 스레드
1) 프로세스
> 프로세스란 작성한 코드의 데이터가 메모리에 올라 실행되는 단위
> 최근 아두이노를 활용한 프로젝트에서 노트북 1대로 각각 인증용 프로그램, 서버, 클라이언트를 모두 실행했는데 이 경우 프로세스를 3개 사용한 것이라 볼 수 있다
2) 스레드
> 스레드는 프로세스 내부의 개념으로써 프로세스 안에 여러 개의 스레드가 존재할 수 있는 것이 특징이다
> 여태까지 사용했던 Main이 대표적인 스레드라고 볼 수 있고 일반적으로 이것을 메인 스레드라고 부른다
> 메인 스레드 외에 다른 스레드를 생성했다면 메인 스레드와 여러 개의 서브 스레드가 있는 것

▲ 메인 스레드 하나만 있는 경우(Main)

▲ 메인 스레드 뿐만 아니라 여러 개의 서브 스레드가 있는 경우(Main, Sub1, Sub2 ... Sub N)
2. 스레드 시작
1) 사용법
static void DoSomething(){~} // 스레드 실행 메서드 정의
Thread t1 = new Thread(new ThreadStart(DoSomething)); // 스레드 객체 생성 // 사용할 메서드와 함께 대리자 객체를 인수로 넘김
t1.Start(); // 스레드 시작
t1.Join(); // 스레드 종료대기
▲ 순서
: 스레드 실행 메서드 정의 -> 스레드 객체 생성 -> 스레드 실행 메서드와 함께 대리자 객체를 인수로 넘김 -> 스레드 시작 -> 스레드 종료대기
3. 스레드 종료
1) Abort()
> 스레드를 임의종료시키기 위해서는 Abort() 메서드를 호출한다.
Thread t1 = new Thread(new ThreadStart(DoSomething)); // 스레드 객체 생성
t1.Start(); // 스레드 시작
t1.Abort(); // 스레드 임의 종료
t1.Join(); // 스레드 종료대기
> 스레드 임의종료의 경우에 ThreadAbortException 이라는 예외를 던지는데 이 예외를 처리할 수 있다면 처리를 마무리하고, 아닌 경우에는 스레드를 종료시킴
2) Interrupt()
> Abort와 가장 큰 차이점은 Interrupt()는 스레드가 동작 중(Running)일 때는 종료시키지 않고, 쉬는 중(WaitSleepJoin)일 때만 종료시킨다는 점임.
Thread t1 = new Thread(new ThreadStart(DoSomething)); // 스레드 객체 생성
t1.Start(); // 스레드 시작
t1.Interrupt(); // 스레드 임의 종료
t1.Join(); // 스레드 종료대기
4. 스레드 상태
> 스레드는 여러 상태를 가질 수 있음
- Running : 스레드 동작 중
- WaitSleepJoin : 스레드 블록 상태(Thread.Sleep(), Thread.Join() 호출 시 이 상태가 됨)
- Aborted : 스레드 취소 상태(이후 Stopped 상태로 전환되어 완전히 중지됨)
- Stopped : 스레드 중단 상태
static void Main()
{
Thread t1 = new Thread(new ThreadStart(DoSomething));
t1.Start(); // t1 스레드는 Running 상태
Console.WriteLine("스레드 실행 중");
// 호출로 메인 스레드는 WaitSleepJoin 상태, 이후 t1 스레드가 Stopped 상태가 되면 다시 메인 스레드는 Runnig 상태가 됨
t1.Join();
}
5. 스레드 간 동기화
> 스레드의 가장 큰 장점이 같은 프로세스 내 스레드끼리는 자원(전역변수)을 공유할 수 있다는 점인데, 이는 동시에 단점이 될 수 있다
> 단점은 한 스레드가 이 자원에 접근했을 때 다른 스레드가 접근을 하게되어 이 자원을 멋대로 바꿔 놓는다면 의도한 행동과 다르게 잘못된 결과를 불러올 수 있음
> 따라서 이러한 단점을 해결할 수 있는 것이 "동기화"이고, 구체적으로는 한 스레드가 자원을 사용할 때에는 다른 스레드에서 접근할 수 없도록 하는 것을 의미한다
1) lock
> lock 키워드를 통해서 원하는 로직을 여러 스레드가 함부로 접근할 수 없도록 만들 수 있다
private readonly object thisLock = new object(); // lock용 매개변수 생성
public void func()
{
// 매개변수를 넣어 lock 키워드를 사용
// 이제부터 아래 블록은 한 스레드가 접근시 다른 스레드 접근 불가
lock(thisLock)
{
~
}
}
2) Monitor(Enter, Exit)
> lock과 다른 점은 수동 제거를 해줘야 한다는 점
private readonly object thisLock = new object(); // lock용 매개변수 생성
public void func()
{
Monitor.Enter(thisLock); // lock처럼 사용
try{};
finally{ Monitor.Exit(thisLock); } // lock과 달리 없애줘야 함
}
3)Monitor.Wait, Monitor.Pulse
> 모든 스레드들은 Ready Queue에 입력된 후에 lock 객체를 얻으며 개별적인 작업을 하는 구조로 되어 있다.
> Monitor.Wait을 호출하게 되면 스레드를 WaitSleepJoin 상태로 변경하고 가지고 있던 lock 객체를 내려놓은 후 Waiting Queue에 입력되게 됨.
> 이 상태는 다른 스레드가 lock 객체를 획득할 수 있는 상태가 되어 다른 스레드가 접근을 할 수 있는 상태가 된다
> 이후 위 스레드가 Monitor.Pulse를 호출하게 되면 Waiting Queue의 가장 첫번째 위치의 스레드를 꺼내어 Ready Queue에 입력하게 됨
> 맨 위의 Ready Queue 작동 방식에 따라 스레드는 작업을 시작한다.
private readonly object thisLock = new object(); // lock용 매개변수 생성
bool locked = false; // 스레드의 WaitSleepJoin 상태를 추적할 변수
public void func()
{
lock(thisLock)
{
// 2. 다른 스레드는 변경된 locked 상태 조건 때문에 Monitor.Wait에 걸려 WaitSleepJoin 상태가 됨
// 5. 다시 Ready Queue에 입력된 스레드는 다른 스레드들과 함께 동작한다.
while(locked == true) Monitor.Wait(thisLock);
// 1. 첫 스레드는 While 조건을 건너뛰어 locked를 true로 변경한다
locked = true;
// yourLogic
// 3. 첫 스레드가 위의 로직(yourLogic)을 모두 수행한 뒤 다시 locked 상태 조건을 변경한다.
locked = false;
// 4. Monitor.Pulse를 통해 WaitSleepJoin 상태에 있던 스레드를 Ready Queue에 입력
Monitor.Pulse(thisLock);
}
6. Task
> Task는 thread의 고성능 버전임
> thread의 문제는 사용되지 않는데도 리소스를 소비한다는 점이 가장 큼(성능 하락의 문제)
> 따라서 필요한 thread를 최소한으로 생성하되 비동기적으로 처리하는 것이 필요하다
> thread 수를 사전에 조절하여 갯수를 제한하는 ThradPool Queue에 Task는 내부적으로 등록되고, ThreadPool은 시스템 코어에 비례하여 스레드 수를 조정한다
> 결국 thread 수는 최적으로 맞추면서 동시에 비동기적으로 사용할 수 있기 때문에 Taks는 고성능 thread이다
1) 기본 사용법
Task my_task = new Task(someAction); // someAction은 반환형이 없는 메서드 또는 익명 메서드 또는 무명함수
my_task.Start(); // thread.Start()와 비슷함
// yourLogic
myTask.Wait(); // thread.Join()과 비슷함
▲ thread와 유사하게
var my_task = Task.Run(someAction); // Run은 생성과 시작을 동시에 함
// yourLogic
my_task.Wait();
▲ 생성과 시작을 동시에 할 수 있는 Run을 활용
2) Task<TResult>
> Task와 가장 큰 차이점은 비동기 실행 코드를 Action 대리자가 아닌 Func 대리자로 받는다는 점, 결과를 반환받을 수 있다는 점임
> 참고로 Action과 Func는 무명함수(람다식을 이용한 익명메서드를 따로 부르는 말)를 만드는 키워드임.
var my_task = Task.Run<List<int>>(() => // 람다식 사용하여 Task<TResult> 생성 및 시작
{
var result = new List<int>();
for (int i = 1; i <= 5; i++)
{
result.Add(i * i);
}
return result;
});
List<int> numbers = my_task.Result; // 결과를 반환받기
7. async / await
> 이들은 비동기화를 할 수 있는 새로운 키워드임
1) 기본문법
public static async Task MyMethod() // 메서드에 async 키워드가 붙는다
{
// async 메서드에는 await이 필수로 붙는다.
// 없는 경우 동기적으로 실행되므로 async의 의미가 없음.
await Task.Run(YourMethod);
}
> async 연산자가 붙은 메서드를 호출하게 되면, async 메서드는 블록을 순서대로 실행시키며 await을 찾게되고 만나게 되면 호출자에게 제어를 돌려주며 자신의 블록을 계속 실행시킨다.
> 제어권을 받은 호출자는 자신의 블록을 실행시킨다.
> 따라서 async와 await을 기준으로 호출자 블록과 asnyc 블록을 동시에 처리하게 되므로 비동기적임.
2) 사용법
using System;
using System.Threading.Tasks;
namespace Async
{
class MainApp
{
async static private void MyMethod(int count)
{ // async 메서드 생성
Console.WriteLine("async.Hello");
await Task.Run(async () => // 람다식 이용한 await 키워드 사용
{
for (int i = 1; i < count; i++)
{
Console.WriteLine($"yourCount : {i}");
await Task.Delay(100);
}
});
Console.WriteLine("async.Bye");
}
static void Caller()
{ // async 메서드와 비동기적으로 사용될 메서드
Console.WriteLine("Caller.Hello");
MyMethod(7);
for (int i = 0; i < 7; i++)
{
Console.WriteLine($"Caller.Bye : {i}");
}
}
static void Main(string[] args)
{
Caller();
Console.ReadLine();
}
}
}


> 근소하지만 어느정도 차이가 있음
'LMS 7 > 개발일지' 카테고리의 다른 글
| 25.10.02 개발일지 / C# 4(2) (Chapter20~22) (0) | 2025.11.11 |
|---|---|
| 25.10.01 개발일지 TCP/IP 스택 (0) | 2025.11.11 |
| 25.09.30 개발일지 / C# 클래스 (0) | 2025.11.11 |
| 25.09.29 개발일지 / C# 3 (Chapter13) (0) | 2025.11.11 |
| 25.09.29 개발일지 / C# 2(3) (Chapter11, Chapter12) (0) | 2025.11.11 |