2022년 10월 25일 화요일

c# async 와 await 이야기

[개념]

c# async await 처리기법은 반응시간에 여유를 준다.
웹에서는 서버에서 동시접속할때 부하를 여러 쓰레드로 나누어 처리하고
앱에서는 GUI 프리징을 막아준다.
근대화된 언어에서 위 기법으로 도배가 되어있지 않으면
퍼포먼스가 떨어진다. 멀티코어시대에 자원활용을 충분히 못한다고 할수 있다.


[용어와 문법정리]

일례로 c#에서 어떤 실행단위를 Task로 묶어서 작업을 할때
Task.Run(실행메서드)    이거나  Task.Factory.StartNew(실행메서드)
(내부로 메서드를 전달하는게 귀찮지만 Run은 클로저로 바로참조가능함)

이게 있는데 둘이 내부적으로는 같은 메커니즘인데
Task.Run이 좀더 간소화된 표현으로써 사용된다.
그리고 하나하나 줄줄이 사탕으로 엮어서 처리할때는

Task.Run(처음할일).ContinueWith(두번째할일).ContinuWith(세번째할일)

요런식으로해서 엮기가 가능하다..  각할일이 끝나면 결과를
다음할일의 인수로 줄수도있다.  (인수) => { 할일기술  }

요로코롬


그리고 C# 라이브러리 중에  ***Async로 된것은 내부적으로
async await 을 한다음에 결과를 Task로 돌려준다.
(아닐수도 있으니 선언을 잘보면서 ㄱㄱ)


명시적으로 new Task를 생성한경우  ->  new Task (할일)의 경우
Task는 작업이 시작되지 않는다.  start로 실행해야함


반면 비동기 async 메서드를 호출하면 작업은 이미 시작된 상태이다.. 
결과는 Task이니까 결과를 기다리던 결과를 unpack해서 변수에 넣건
알아서 하면됨 다만 작업은 시작되었으니까 무한루프가 있거나
주쓰레드가 종료되지 않는 이상 언젠가는 끝남



var my_task = new Task(async () => { return 1; });
이렇게 되어있을때도
선언만된 케이스이다. 따라서 결과 값을 얻으려거든

int aa = my_task.Result;
혹은
int aa = my_task.GetAwaiter().GetResult();

이렇게 사용하여 결과를 얻으면 된다. (후자추천)


awiat은 연산자이다. 
await의 피연산자는 Task 객체 혹은 Task객체를 반환하는 메서드가 온다.
await의 피연산자가 해당작업이 반환값이 있는 Task<T>인 경우 Task에 저장된
Result를 unpack 까지해서 결과타입을 반환해준다.


[설계방향]

Task가 내부적으로 쓰레드를 사용한다고 해서 백쓰레드와 사용처도 같다고 
생각하면 안된다.
async await 처리는 어떤 함수가 있었을때 특정코드 구간이 
시간이 오래걸린다고 판단되면(0.2초이상) 이 실행구역을 Task 단위로 
묶어버리고 await 키워드를 걸어놓으면 cpu가 해당 작업을 수행할때
기다리지않고 다른일을 하러간다. 
이행동으로 반응 퍼포먼스를 향상시킴이 목적이고 해당 task가 한메서드 안에 
있으면 클로저 외부 변수를 참조할수 있으니까 변수참조가 가능하다.

반면에 백쓰레드는 주쓰레드에서 하는 일과는 별개로 어떤 작업을 너는너 나는나
이렇게 수행해야할때 사용하면된다.  주쓰레드에 일어나는 일의 변수를 
별도 쓰레드한테 전달할때 쓰레드 동기화 기법으로 전달하거나 전달받으면 된다.


비슷하지만 사용처가 다르다..
서로다른 Task에서 같은 변수를 접근하려 할때는 lock을 사용하지 못한다. (catch finally unsafe 도 await으로 결과받기 안됨)
대신 이경우에는 세마포어나 이벤트셋을 이용해서 처리할수는 있다.
근데 이렇게 가는건 설계가 조금 이상한거고 작업으로 분리된다는 거는
다중 Task의 동시 취합을 기다렸다가 다음 실행구문으로 넘기는 방향으로 
설계가 되어야한다.  아니면 병렬처리로 가던가..


[반환형에 대하여..]

async 메서드가 반환하는게 없으면 void 이거나 Task 이고
특정 자료형이면 Task<T> 이다..

오해할수 있는게 async 메서드가 void를 반환 한다고 해서 무조건 
Task로 명시하지 않아도 된다는거다.

아무것도 반환하지 않을때 void 일수도 있고 그냥 Task일수도 있다.

둘의 차이는
void 반환이면 콜러가 await을 할수 없다.
Task 반환이면 콜러가 await을 할수 있다.
그래서 void리턴은 콜러가 반환을 기다리지 않는다.

이정도임 단지 void를 리턴하여 사용하는경우가 Task를 리턴하여 
사용하는것보다 사용빈도가 낮다.. 
이유는 결과를 기다리지 않고 바로 진행하면 문제가 될수 있기 때문에

특정 이벤트 핸들러에서 사용하는 정도에서 그친다고 보면 된다. 
(예를 들어 버튼클릭 이벤트에 할당한다거나 이런경우..)

[알아두어야할사항]

async 메서드를 정의할때 비동기(async)라고 명시 되어있고
비동기로 실행될수 있는 영역이지만 해당메서드 안에서 
await을 사용하지 않으면 비동기처럼 동작하지 않는다.
즉 async 메서드로 선언하고 await을 내부에서 사용하지 않으면
(물론 그럴수는 있지만!!) 이건 비동기 측면에서 바라봤을때 
아무짝에도 쓸모가 없다는 애기다.

정확히는 async 메서드 안에서 await 키워드를 만나기 전에는 동기로 
실행되다가 await을 만나는 순간 비동기 영역으로 바뀐다.
유의 할점은 await을 만나 어떤 값을 기다려서 반환 받았더라도 
그이후의 코드를 처리함에 있어서 앞서서 await을 만났기 때문에 
이영역도 비동기 영역으로 처리된다. 라는 것이다.
비동기 영역이므로 await을 만나면서 분리가 되었고 await Task가 끝나고
분리된 영역에서 다음을 수행하기때문에 그렇다고 이해하면 될것 같다.
논리적으로도 한번 분리된 실행영역이 시점상 다시 원점으로 돌아오는것이
그냥 비동기랑 같기 때문에 이렇게 되는것이다.

그리고 여러개의 Task를 병렬처리할때
var tasks = new List<Task<T>>();
tasks.Add(Task.Run(() => {  할일  }));
로 여러개의 일을 시키고나서 이결과를 기다릴때 쓰는 메서드는

Task.WaitAll / Task.WaitAny
Task.WhenAll / Task.WhenAny

이렇게 있다.  이 2종류의 차이는 Wait** 는 동기적으로 기다린다.
그리고 When**는 비동기 적으로 기다린다. (그래서 전방에 await을 달수 있다.)
Wait을 사용할때 전방에 await이 없는 경우 동기적으로 실행되니까
When을 하고 await을 걸어서 전부 혹은 한개라도 작업의 완성을 기다릴수 있다.
(항상 async/await 처리할때 Wait 어쩌고 함수가 붙어 있으면 현재쓰레드에서
실행된다고 간주하고 처리해야 DeadLock 현상을 막을수 있다.)

이 2가지 메서드는 잘만사용하면 정말 요소요소에서 잘써먹을수 있는 메서드이다.
활용도 200%

그리고 Task 메서드 체이닝으로 ConfigureAwait()  메서드를 사용할수 있다. 
UI 어플리케이션에서 (윈폼이나 WPF 같은..)는 UI쓰레드와 동기화 이슈가 있을수 있다.
콘솔이나 asp.net core 같은 웹프레임워크에서는 UI동기화 Context가 없다.
하지만 위에 언급된 GUI어플리케이션에서는 async await를 처리하고 난후
UI와 동기화 하여 처리할때 어려방법이 있지만 (InvokeRequire같은..)
하지만 기본적으로 UI어플리케이션을 위한 솔루션은 Task 메서드 체이닝으로
ConfigureAwait(bContext); 를 수행하므로써 동기화 할수 있다.

내부적으로는 ConfigureAwait(true) 로 하면 awiat문 앞에서 사용된것과 동일한
스레드에서 실행되고 ConfigureAwait(false)로 하면 Task가 사용가능한
쓰레드풀을 찾아 그 쓰레드에서 실행된다.  기본값은 true 이다.

그래서 UI쓰레드에서 호출하는 함수에서 await으로 분기처리하고 체이닝을 통해
ConfigureAwait(false)로 처리하고 그다음 수행동작에서 UI쓰레드의 자원을
이용하려고 하면 에러가 나는 이유가 ConfigureAwait(false)로 다른쓰레드풀로써의
분기가 일어나고 그쓰레드에서 계속 UI쓰레드 자원을 접근하는 코드를 수행하려
했기때문이다.



댓글 없음:

댓글 쓰기