Microsoft MVP성태의 닷넷 이야기
닷넷: 2138. C# - async 메서드 호출 원칙 [링크 복사], [링크+제목 복사],
조회: 12875
글쓴 사람
정성태 (techsharer at outlook.com)
홈페이지
첨부 파일
 
(연관된 글이 1개 있습니다.)
(시리즈 글이 12개 있습니다.)
.NET Framework: 698. C# 컴파일러 대신 직접 구현하는 비동기(async/await) 코드
; https://www.sysnet.pe.kr/2/0/11351

.NET Framework: 716. async 메서드의 void 반환 타입 사용에 대하여
; https://www.sysnet.pe.kr/2/0/11414

.NET Framework: 717. Task를 포함하지 않는 async 메서드의 동작 방식
; https://www.sysnet.pe.kr/2/0/11415

.NET Framework: 719. Task를 포함하는 async 메서드의 동작 방식
; https://www.sysnet.pe.kr/2/0/11417

.NET Framework: 731. C# - await을 Task 타입이 아닌 사용자 정의 타입에 적용하는 방법
; https://www.sysnet.pe.kr/2/0/11456

.NET Framework: 737. C# - async를 Task 타입이 아닌 사용자 정의 타입에 적용하는 방법
; https://www.sysnet.pe.kr/2/0/11484

.NET Framework: 813. C# async 메서드에서 out/ref/in 유형의 인자를 사용하지 못하는 이유
; https://www.sysnet.pe.kr/2/0/11850

닷넷: 2138. C# - async 메서드 호출 원칙
; https://www.sysnet.pe.kr/2/0/13405

닷넷: 2147. C# - 비동기 메서드의 async 예약어 유무에 따른 차이
; https://www.sysnet.pe.kr/2/0/13421

닷넷: 2318. C# - (async Task가 아닌) async void 사용 시의 부작용
; https://www.sysnet.pe.kr/2/0/13884

닷넷: 2319. ASP.NET Core Web API / Razor 페이지에서 발생할 수 있는 async void 메서드의 부작용
; https://www.sysnet.pe.kr/2/0/13885

닷넷: 2321. Blazor에서 발생할 수 있는 async void 메서드의 부작용
; https://www.sysnet.pe.kr/2/0/13888




C# - async 메서드의 호출 원칙

async 메서드에 대해 지켜야 할 가장 기본적인 원칙은, "async all the way"라는 것입니다.

즉, async 메서드인 경우 "특별한 예외"가 없는 한 "await" 호출을 하면 됩니다. 다른 말로 하면, async 메서드를 일반 메서드처럼 호출하지는 말라는 의미입니다.

예를 들어, 대상 메서드가 async로 되어 있다면 (거의 무조건) await으로 호출합니다.

await Do();            

private async Task Do()
{
    Console.WriteLine("Do");
}

Visual Studio의 인텔리센스로 본다면 아래와 같이 "(awaitable)"이라고 붙은 메서드가 이에 해당합니다.

call_awatiable_1.png

저런 async 메서드를 "await" 없이 호출하는 것은 의미가 없습니다. 왜냐하면 await 호출을 가정하고 만든 것이기 때문에 단순히 동기 방식으로 호출하는 것은 자칫 프로그램의 흐름을 쉽게 이해하지 못하도록 만들어버립니다.




async 메서드를, 혹은 단순히 Task만 반환해도 awaitable을 만족하는데, 그런 메서드에 대해 동기식으로 호출해야 하는 상황이 분명히 있긴 합니다.

예를 들어, 네트워크로 로깅을 하는 코드를 가정했을 때, 로깅 자체의 지연을 프로그램의 성능에 넣고 싶지 않아 비동기로 만들었다고 가정해 보면 이렇게 호출하고 싶을 것입니다.

NLogAsync("test");

async Task NLogAsync(string text)
{
    // ...네트워크 비동기 쓰기...
}

하지만 직관성을 염두에 둔다면, 개발자들은 NLogAsync에 대해 "await ..." 호출을 하려고 들 것이므로 애당초 저 메서드를 async로 표현하지 않는 것이 더 좋습니다.

NLogAsync("test");

void NLogAsync(string text) // 자연스럽게 개발자는 이 메서드에 대해 await 호출 대상이라고 여기지 않음
{
    // ...네트워크 비동기 쓰기...
}




현재, (개인적으로) 유일하게 async 메서드를 await으로 직접 호출하지 않을 실용적인 사례는 "병렬" 처리일 때입니다. 예를 들어, DB 쓰기를 2개의 데이터베이스에 수행해야 한다고 가정해 보겠습니다.

await DBWriteAsync("[db1 연결문자열]");
await DBWriteAsync("[db2 연결문자열]");

async Task DBWriteAsync(string connectionString)
{
    // DB 비동기 쓰기 (1초 소요)
}

위와 같이 DBWriteAsync를 2번 수행하면 총 수행 시간은 2초가 걸립니다. 바로 이런 경우, (의존성이 없어 병렬로 수행할 수 있다면) 직접 호출한 후 Task.WhenAll 메서드와 곁들여 처리하면 됩니다.

Task task1 = DBWriteAsync("[db1 연결문자열]");
Task task2 = DBWriteAsync("[db2 연결문자열]");

Task.WhenAll(task1, task2);

위와 같이 해주면 DBWriteAsync 2번의 호출이 연이어 호출되므로 총 작업 시간은 1초에 끝나게 됩니다.

하지만, 저것 역시 엄밀히는 "async all the way" 방식에 부합하지 않습니다. 왜냐하면 "Task.WhenAll(...)" 호출은 그 내부에서 이뤄지는 비동기를 무시하고 바로 반환하기 때문에 역시 이후의 처리에서 코드 수행 순서가 복잡해집니다.

따라서 WhenAll까지 await 호출을 해줘야 비로소 진정한 비동기 처리의 완성이 되는 것입니다.

await Task.WhenAll(task1, task2); // task1, task2 완료 후에 다음 코드를 수행하도록 비동기 호출




실제로 제가 지금까지 종종 받아온 async/await 질문 중에는 async 메서드에 대해 그냥 (await 없이) 호출하면서 프로그램의 흐름이 잘 이해되지 않는다는 글들이 있었는데요, 올바른 사용법이 아니므로 엄밀히는 그 흐름을 굳이 이해하려고 애쓸 필요가 없습니다.

마침 아래의 질문도 이와 유사합니다.

Thread.Sleep(500), await Task.Delay(500), Task.Delay(500) 차이점이 궁금합니다.
; https://www.sysnet.pe.kr/3/0/5916

본문의 코드에 동기 호출과 비동기 호출을 함께 담아 예제를 구성하고 있는데요, 엄밀히는 이런 예제는 현실성이 없습니다.

즉, 동기/비동기로 나누는 경우 예제 코드가 아래와 같이 분명한 차이를 보여야 하는 것입니다.

// countDown을 동기로 만든 경우,

private void button1_Click(object sender, EventArgs e)
{
    countDown();
    countDown();
    MessageBox.Show("Done");
}

private void countDown()
{
    for (int i = 9; i >= 0; i--)
    {
        textBox1.Text += i.ToString();
        Thread.Sleep(500);
    }
}

// countDown을 비동기 및 병렬 처리로 수행하는 경우

private async void button1_Click(object sender, EventArgs e)
{
    var x = countDownAsync();
    var y = countDownAsync();
    await Task.WhenAll(x, y);
    MessageBox.Show("Done");
}

private async Task countDownAsync()
{
    for (int i = 9; i >= 0; i--)
    {
        textBox1.Text += i.ToString();
               
        await Task.Delay(500);
    }
}

결국, "3번" 상황은 그냥 잊어버리셔도 됩니다.

// "async all the way" 원칙에 맞지 않게 코딩한 경우

private async Task countDown()
{
    for (int i = 9; i >= 0; i--)
    {
        textBox1.Text += i.ToString();
        Task.Delay(500);
    }
}

현실적으로 저렇게 사용하는 경우는 없어야 합니다. 물론, 의도적으로 그렇게 하는 경우도 있겠지만... 다른 사람들의 코드 리딩을 돕기 위해서라면 저런 경우는 그냥 다음과 같이 만드는 것이 더 직관적입니다.

private void countDown()
{
    for (int i = 9; i >= 0; i--)
    {
        textBox1.Text += i.ToString();
        // Task.Delay(500);
        Thread.Sleep(500);
    }
}

비록 공부하는 단계에서 저 3가지 경우를 엮어서 1개의 코드베이스로 해석하고 싶겠지만, 현실적으로는 그다지 권장하지 않는 접근 방식입니다.




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 1/26/2024]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 




... 46  47  48  49  50  51  [52]  53  54  55  56  57  58  59  60  ...
NoWriterDateCnt.TitleFile(s)
12635정성태5/9/202116308기타: 81. OpenTabletDriver를 (관리자 권한으로 실행하지 않고도) 관리자 권한의 프로그램에서 동작하게 만드는 방법
12634정성태5/9/202114788개발 환경 구성: 572. .NET에서의 필수 무결성 제어 - 외부 Manifest 파일을 두는 방법파일 다운로드1
12633정성태5/7/202117702개발 환경 구성: 571. UAC - 관리자 권한 없이 UIPI 제약을 없애는 방법
12632정성태5/7/202118916기타: 80. (WACOM도 지원하는) Tablet 공통 디바이스 드라이버 - OpenTabletDriver
12631정성태5/5/202117745사물인터넷: 60. ThingSpeak 사물인터넷 플랫폼에 ESP8266 NodeMCU v1 + 조도 센서 장비 연동파일 다운로드1
12630정성태5/5/202118527사물인터넷: 59. NodeMCU v1 ESP8266 보드의 A0 핀 사용법 - CdS Cell(GL3526) 조도 센서 연동파일 다운로드1
12629정성태5/5/202120257.NET Framework: 1057. C# - CoAP 서버 및 클라이언트 제작 (UDP 소켓 통신) [1]파일 다운로드1
12628정성태5/4/202118176Linux: 39. Eclipse 원격 디버깅 - Cannot run program "gdb": Launching failed
12627정성태5/4/202118278Linux: 38. 라즈베리 파이 제로 용 프로그램 개발을 위한 Eclipse C/C++ 윈도우 환경 설정
12626정성태5/3/202118381.NET Framework: 1056. C# - Thread.Suspend 호출 시 응용 프로그램 hang 현상 (2)파일 다운로드1
12625정성태5/3/202116858오류 유형: 714. error CS5001: Program does not contain a static 'Main' method suitable for an entry point
12624정성태5/2/202121324.NET Framework: 1055. C# - struct/class가 스택/힙에 할당되는 사례 정리 [10]파일 다운로드1
12623정성태5/2/202117665.NET Framework: 1054. C# 9 최상위 문에 STAThread 사용 [1]파일 다운로드1
12622정성태5/2/202113503오류 유형: 713. XSD 파일을 포함한 프로젝트 - The type or namespace name 'TypedTableBase<>' does not exist in the namespace 'System.Data'
12621정성태5/1/202118404.NET Framework: 1053. C# - 특정 레지스트리 변경 시 알림을 받는 방법 [1]파일 다운로드1
12620정성태4/29/202121520.NET Framework: 1052. C# - 왜 구조체는 16 바이트의 크기가 적합한가? [1]파일 다운로드1
12619정성태4/28/202121532.NET Framework: 1051. C# - 구조체의 크기가 16바이트가 넘어가면 힙에 할당된다? [2]파일 다운로드1
12618정성태4/27/202119777사물인터넷: 58. NodeMCU v1 ESP8266 CP2102 Module을 이용한 WiFi UDP 통신 [1]파일 다운로드1
12617정성태4/26/202117060.NET Framework: 1050. C# - ETW EventListener의 Keywords별 EventId에 따른 필터링 방법파일 다운로드1
12616정성태4/26/202116745.NET Framework: 1049. C# - ETW EventListener를 상속받았을 때 초기화 순서파일 다운로드1
12615정성태4/26/202114009오류 유형: 712. Microsoft Live 로그인 - 계정을 선택하는(Pick an account) 화면에서 진행이 안 되는 문제
12614정성태4/24/202118306개발 환경 구성: 570. C# - Azure AD 인증을 지원하는 ASP.NET Core/5+ 웹 애플리케이션 예제 구성 [4]파일 다운로드1
12613정성태4/23/202116656.NET Framework: 1048. C# - ETW 이벤트의 Keywords에 속한 EventId 구하는 방법 (2) 관리 코드파일 다운로드1
12612정성태4/23/202116545.NET Framework: 1047. C# - ETW 이벤트의 Keywords에 속한 EventId 구하는 방법 (1) PInvoke파일 다운로드1
12611정성태4/22/202115439오류 유형: 711. 닷넷 EXE 실행 오류 - Mixed mode assembly is build against version 'v2.0.50727' of the runtime
12610정성태4/22/202115378.NET Framework: 1046. C# - 컴파일 시점에 참조할 수 없는 타입을 포함한 이벤트 핸들러를 Reflection을 이용해 구독하는 방법파일 다운로드1
... 46  47  48  49  50  51  [52]  53  54  55  56  57  58  59  60  ...