Microsoft MVP성태의 닷넷 이야기
닷넷: 2138. C# - async 메서드 호출 원칙 [링크 복사], [링크+제목 복사],
조회: 13168
글쓴 사람
정성태 (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

비밀번호

댓글 작성자
 




... 91  92  93  94  95  96  97  98  99  100  101  102  [103]  104  105  ...
NoWriterDateCnt.TitleFile(s)
11358정성태11/15/201726622사물인터넷: 9. Visual Studio 2017에서 Raspberry Pi C++ 응용 프로그램 제작 [1]
11357정성태11/15/201727123개발 환경 구성: 336. 윈도우 10 Bash 쉘에서 C++ 컴파일하는 방법
11356정성태11/15/201728679사물인터넷: 8. Raspberry Pi Zero(OTG)를 다른 컴퓨터에 연결해 가상 마우스 + 키보드로 쓰는 방법 [4]
11355정성태11/15/201724501사물인터넷: 7. Raspberry Pi Zero(OTG)를 다른 컴퓨터에 연결해 가상 마우스로 쓰는 방법 [2]파일 다운로드2
11354정성태11/14/201728667사물인터넷: 6. Raspberry Pi Zero(OTG)를 다른 컴퓨터에 연결해 가상 키보드로 쓰는 방법 [8]
11353정성태11/14/201725869사물인터넷: 5. Raspberry Pi Zero(OTG)를 다른 컴퓨터에 연결해 가상 이더넷 카드로 쓰는 방법 [1]
11352정성태11/14/201721959사물인터넷: 4. Samba를 이용해 윈도우와 Raspberry Pi간의 파일 교환 [1]
11351정성태11/7/201725213.NET Framework: 698. C# 컴파일러 대신 직접 구현하는 비동기(async/await) 코드 [6]파일 다운로드1
11350정성태11/1/201721183디버깅 기술: 108. windbg 분석 사례 - Redis 서버로의 호출을 기다리면서 hang 현상 발생
11349정성태10/31/201721650디버깅 기술: 107. windbg - x64 SOS 확장의 !clrstack 명령어가 출력하는 Child SP 값의 의미 [1]파일 다운로드1
11348정성태10/31/201718138디버깅 기술: 106. windbg - x64 역어셈블 코드에서 닷넷 메서드 호출의 인자를 확인하는 방법
11347정성태10/28/201721730오류 유형: 424. Visual Studio - "클래스 다이어그램 보기" 시 "작업을 완료할 수 없습니다. 해당 인터페이스를 지원하지 않습니다." 오류 발생
11346정성태10/25/201718284오류 유형: 423. Windows Server 2003 - The client-side extension could not remove user policy settings for 'Default Domain Policy {...}' (0x8007000d)
11338정성태10/25/201716690.NET Framework: 697. windbg - SOS DumpMT의 "BaseSize", "ComponentSize" 값에 대한 의미파일 다운로드1
11337정성태10/24/201718820.NET Framework: 696. windbg - SOS DumpClass/DumpMT의 "Vtable Slots", "Total Method Slots", "Slots in VTable" 값에 대한 의미파일 다운로드1
11336정성태10/20/201719593.NET Framework: 695. windbg - .NET string의 x86/x64 메모리 할당 구조
11335정성태10/18/201718599.NET Framework: 694. 닷넷 - <Module> 클래스의 용도
11334정성태10/18/201719639디버깅 기술: 105. windbg - k 명령어와 !clrstack을 조합한 호출 스택을 얻는 방법
11333정성태10/17/201718794오류 유형: 422. 윈도우 업데이트 - Code 9C48 Windows update encountered an unknown error.
11332정성태10/17/201719804디버깅 기술: 104. .NET Profiler + 디버거 연결 + .NET Exceptions = cpu high
11331정성태10/16/201718147디버깅 기술: 103. windbg - .NET 4.0 이상의 환경에서 모든 DLL에 대한 심벌 파일을 로드하는 파이썬 스크립트
11330정성태10/16/201717406디버깅 기술: 102. windbg - .NET 4.0 이상의 환경에서 DLL의 심벌 파일 로드 방법 [1]
11329정성태10/15/201721522.NET Framework: 693. C# - 오피스 엑셀 97-2003 .xls 파일에 대해 32비트/64비트 상관없이 접근 방법파일 다운로드1
11328정성태10/15/201724439.NET Framework: 692. C# - 하나의 바이너리로 환경에 맞게 32비트/64비트 EXE를 실행하는 방법파일 다운로드1
11327정성태10/15/201718207.NET Framework: 691. AssemblyName을 .csproj에서 바꾼 경우 빌드 오류 발생하는 문제파일 다운로드1
11326정성태10/15/201718512.NET Framework: 690. coreclr 소스코드로 알아보는 .NET 4.0의 모듈 로딩 함수 [1]
... 91  92  93  94  95  96  97  98  99  100  101  102  [103]  104  105  ...