2023. 10. 24. 22:36ㆍC#
오랜만에 글 한편 쓴다
오늘은 C# 비동기 프로그래밍의 핵심 개념 중 하나인 Task.Run의 사용과 그냥 메서드를 직접 호출하는 방법의 차이점에 대해 이야기해볼려고 하는데 이거 존나 중요하니까 잘 보도록 하자
public COMPortExplorerVM()
{
Task.Run(async () => await MonitorComportStatusChangesAsync());
B();
}
public COMPortExplorerVM()
{
MonitorComportStatusChangesAsync();
B();
}
위 두 코드는 어떤 차이점이 있을까?
아참 MonitorComportStatusChangesAsync() 의 구현부는 아래와 같다고 가정하자.
public async Task MonitorComportStatusChangesAsync()
{
AA();
BB();
CC();
await LongIOAsync();
DD();
}
곰곰히 생각해보고 모르겠으면 아래를 보도록 하고 존나 쉽네 싶으면 걍 여자랑 놀러가면 되겠다
1. 실행 쓰레드의 차이
첫 번째 코드에서 Task.Run을 사용하면 MonitorComportStatusChangesAsync 메서드는 별도의 쓰레드(대부분의 경우 ThreadPool의 쓰레드)에서 실행된다. 그러므로 UI 쓰레드와는 별개의 쓰레드에서 비동기 작업이 실행되므로 UI 쓰레드의 차단을 피할 수 있다
반면에 두 번째 코드에서는 MonitorComportStatusChangesAsync 메서드가 현재 실행 중인 쓰레드(예: UI 쓰레드)에서 바로 실행되므로. 이 경우 await 키워드를 만나기 전까지 현재 쓰레드에서 실행된다.
즉 요약하면 새로운 쓰레드에서 실행되느냐 현재 쓰레드에서 실행되느냐 차이인데, 아직 중요한 부분 안 끝났으니 좀 더 읽어보도록 하자
2. 동작의 흐름
public COMPortExplorerVM()
{
MonitorComportStatusChangesAsync();
B();
}
두번째 코드에 대해 좀 더 자세히 알아보자
두번째 코드는 현재 실행 중인 쓰레드에서 바로 실행된다고 했는데 실행흐름이 어떻게 되는지 한번 알아보면
우선 B() 보다 MonitorComportStatusChangesAsync() 가 먼저 오므로 MonitorComportStatusChangesAsync()가 먼저 실행되는건 당연할거다
그럼 MonitorComportStatusChangesAsync() 내부로 가보면
public async Task MonitorComportStatusChangesAsync()
{
AA();
BB();
CC();
await LongIOAsync();
DD();
}
이런 식으로 되어 있는데 이제 여기서 한번 생각해보자 CPU가 코드를 실행할때 전체 실행 순서는 어떻게 될까?
글 길게 쓰기 귀찮으니까 답을 알려주면 await LongIOAsync() 를 만나기 전까지는 그냥 쭉쭉 실행된다. 그리고 await LongIOAsync()를 만나면 이 시점에서 비동기 실행작업(LongIOAsync)이 완료될때까지 메서드 실행을 중단하고 제어권을 호출자에게 반환한다
그리고 이후 비동기 실행 작업이 마무리 되면 돌아와서 DD() 를 실행하게 된다
그럼 호출자가 누구냐?
COMPortExplorerVM 요놈이다 아 함수이름이 왜 저따구냐고? 그건 내가 사용하던 코드에서 따온거니까 따지지 말고 그냥 보도록 하자
즉 await LongIOAsync() 를 만나면 즉시 제어권은 호출자에게 건너가고 호출자에서 await가 없으니 그 다음 코드가 바로 실행된다.
이제 CPU가 실행하는 코드 순서를 정리해 보면 아래와 같다.
public async Task MonitorComportStatusChangesAsync()
{
AA(); // 2
BB(); // 3
CC(); // 4
await LongIOAsync(); // 5
DD(); // 7 요놈은 바깥의 B() 실행 후에 실행된다
}
public COMPortExplorerVM()
{
MonitorComportStatusChangesAsync(); // 1
B(); // 6 요놈의 실행순서를 잘 보도록 하자
}
자 그럼 이제 마지막으로 아래 코드는?
public COMPortExplorerVM()
{
Task.Run(async () => await MonitorComportStatusChangesAsync());
B();
}
이건 뭐 쉽지 닷넷이 관리해주는 쓰레드 풀 에서 놀고있는 백수 쓰레드 한개를 징집해와서 MonitorComportStatusChangesAsync() 라고 명시된 일을 하라고 착취하는거다.
그러니 당연히 MonitorComportStatusChangesAsync() 와 B() 는 거의 동시에 실행되게 된다 듀얼코어 이상의 환경에서는 일정시간동안은 컨텍스트스위칭없이 말그대로 동시에 돌아갈수도 있고
요약하면
쓰레드풀에서 징집해온 놀고있던 쓰레드 : MonitorComportStatusChangesAsync() 에 배정되서 착취됨
현재 쓰레드 : B() 에 배정되서 착취됨
이후의 일을 좀 얘기해보면 그러다가 MonitorComportStatusChangesAsync() 함수 내부에서 await 를 만나는순간 제어권이 호출자로 넘어가고 호출자에서도 await를 만나게 되는데 이는 최상위 호출자이므로 더이상 갈곳이 없으므로 쓰레드는 휴가받고 자신의 고향 (쓰레드풀)으로 돌아간다.
즉 아래와 같다 보면 된다.

하지만 얼마지나지않아 이와 관련없는 전혀 다른 일 (다른 함수) 로 징집될테고 거서 또 죽도록 노동하다가 await 만나면 await 체인을 따라 최상위 호출자까지 제어권 반환하면서 실행하다 해산 그리고 또 다른일로 징집 이게 반복된다 보면된다. 이게 닷넷 쓰레드의 삶이며 숙명이다.
(여서 중요한건 await 만나서 await 체인을 따라 최상위 호출자까지 거슬러 올라간 후 지금까지 일해왔던 쓰레드가 해산될 경우 이후에 비동기 작업이 끝나고 배정된 쓰레드는 이전의 쓰레드와는 다른 쓰레드일 수도 있단거다, 아니 그럴 가능성이 더 크다)
어쨋든 결론은 이 글 본 이후부터라도 쓰레드 쓸일 있으면 new Thread 쓰지말고 닷넷 정부가 관리해주는 쓰레드 풀에서 백수 쓰레드를 요청 (Task.Run) 하자
글쓰기 귀찮다 대강 알아들어라
'C#' 카테고리의 다른 글
[C#] is 패턴 매칭, is not 패턴 매칭 에 대해 알아보자 (0) | 2023.11.18 |
---|---|
[C#] 비동기 프로그래밍(APM)에 대해 알아보자 (0) | 2023.03.21 |
[C#] 디버깅 시 데이터의 조사식 포맷을 변경해보자 (0) | 2022.12.30 |
[C#] 열거형 값을 문자열로 사용해보자 (0) | 2022.12.30 |
[C#] 외부함수를 멤버함수처럼 사용하기 (0) | 2022.12.26 |