비동기 code의 올바른 사용법

이미 10년 넘게 사용되는 아무생각없이 사용하던 async await 너무 쉽게 남발했던것 같습니다.

https://www.linkedin.com/in/pereiradawid/recent-activity/all/

폴란드 출신의 이 개발자의 도발적인 문구입니다.

이 분 말로는 async await 는 개발할때나 테스트 시에는 모든 테스트를 잘 통과하지만

부하가 걸리면 답이 없다고 합니다.

그 현상은

→ CPU 사용량이 낮은 스레드 풀 고갈
→ 프로덕션의 임의 교착 상태
→ 부하에 따라 기하급수적으로 증가하는 응답 시간
→ 클라우드 비용은 불가사의하게 세 배로 증가합니다.

솔직히 원인은 저렇다고 하지만 저 상태를 프로덕션 상태에서 체크하기 굉장히 어렵고 원인불명의 빠지는 경우가 많습니다.

cpu 사용량 만 보고 정상인데 왜 먹통이 될까 했는데 문제는 쓰레드 고갈이 문제라고 합니다.

한참동안 글을 보면서 느낀거데 async await는 문제라기 보다는

이 와같이 앞의 비동기 메소드의 사용의 예외처리 문제로 문제소지가 많다는 의미인것 같습니다.

평범하고 정상같이 보이지만 앞단의 비동기에서 예외가 발생했을때 뒷단의 처리의 대한 문제소지가

많다는 의미인것 같습니다(맨날 저렇게 코딩했는데 뜨금)

그래서 자신있게 다음날 내가 이 해결책을 말해주겠다면 하루 뜸을 들이고 나타났습니다.

다음날 솔루션으로 제시한 방법의 핵심원칙은

→ 비동기 코드를 차단하지 않음
→ 항상 작업을 반환합니다(비동기 무효 없음).
→ 동시성을 명시적으로 제어
→ 핫 경로에 ValueTask 사용

첫번째 whenall 사용

두번째 ConfigureAwait(false)

세번째 SemaphoreSlim 사용

네번째 value task 사용

이 와같이 비동기 코드를 개선했을때

● 응답 시간: 10-100배 더 빠름
● 처리량: 50-100배 증가
● 스레드 수: 80% 감소
● 메모리 할당: 60% 감소
● 클라우드 비용: 50-70% 절감

이런 효과를 얻을수 있다고 합니다.

5 Likes

제가 사실 DevOps 이런 쪽은잘 모르기는 하지만 그냥 보기에 뭔가…

스레드 고갈에 의해 클라우드 비용이 폭증한다는 주장은 다소 어그로 같습니다. CPU를 소비하지 않는 Idle 상태의 스레드 고갈로 인해서 뭐 오토스케일링 이런걸로 비용이 증가한다면 모를까…

또 하나 서버 환경에서는 기본적으로 SynchronizationContext가 지정되어 있지 않아서 ConfigureAwait(false)를 붙여도 달라지는 부분이 없을 것 같은데 이걸 해결책으로 제시하고 있습니다.

전반적으로 과장되거나 이치에 맞지 않는 부분이 있어 이 내용을 그대로 받아들이긴 어렵지 않나 하는 생각이 듭니다.

다른 분들의 생각은 어떠신가요?

5 Likes

새겨 들을 필요가 있는 것 같기는 한데, 성능 상승 효과는 사실 모르겠습니다.

비동기의 이점은 반응성이지 성능 향상이 아니기 때문에, 작업을 비동기로 처리하든, 동기로 처리하든, 작업량 자체가 많다면 시스템 자원 소비도 많아 지는 물리적 법칙은 비동기 코드의 관습으로 극복할 수 있을까하는 의문이 있습니다.

SemaphoreSlim 을 적용하면, 시스템의 전체 througput 을 제한하는 것과 비슷한 것이라, 병목 현상 때문에 오히려 성능이 저하될 것 같은데, 반대라니 놀랍네요.

그리고, 예외의 처리 관습과 비동기 코드 관습은 별개로 인식되어야 할 것 같습니다.

예를 들면, async void 는 return 을 기다리는 호출자가 명확하지 않을 때 혹은 return 자체를 신경 쓰지 않을 때, 대표적으로 이벤트를 (비동기로) 처리하는 때입니다. 이런 정황에서는, 이벤트 핸들러가 throw 한다는 것은 오히려 더 이상한 것이죠.

그것 보다는, 비동기와 상관 없이, “기대된” 예외는 즉시 에러로 처리 - 예방을 하든, catch를 해서 진짜 "예외"와 구분해야 하는 것이 더 중요할 것 같습니다.

var address = // 주소를 나타내는 문자열

// address 값을 검증하여 에러 예방
if (Uri.IsWellFormedUriString(address, UriKind.Absolute) is false)
{
   // return 
}

// GetAsync 의 예외는 문서에 나와 있기 때문에 기대된 예외 => catch
try {
   using var http = new HttpClient();
   var response = await http.GetAsync(address, token);
   }
catch( // catches 
3 Likes

async await가 비용증가로 이어질 수 있다는 것은 사실인지 좀 더 확인해봐야 될 것 같네요.
이 부분은 누군가 아시는 분이 계시면 그럴 가능성에 대해서 설명해 주시면 좋을 것 같습니다.

글의 내용은 비용 문제를 말하기 보다는 async await의 잘못 된 사용사례를 보여주고 모범 사례를 보여주는 글 같습니다.

@al6uiz 님 말씀 처럼 UI가 없는 서버 환경에서 ConfigureAwait(false)는 성능 향상을 기대할 수 없다고 봐야 될 것 같습니다.
Semaphore를 사용하는 솔루션은 왜 넣었는지 모를만큼 잘 이해가 안가네요??
최대 동시성(아마 CPU 코어의 개수) 만큼만 코드를 진행시키려는 의도 같은데 어차피 엄청 많은 Task를 돌려 쓰레드풀에서 더 이상 가져가 쓸 쓰레드가 없으면 쓰레드를 받을 때가지 기다리는 건 마찬가지 일 것 같아서요..

그리고 I/O Bound 작업이 많을 때는 비동기가 실제로 성능 향상에 큰 도움이 됩니다.
실제로 싱글 스레드 기반인 Node를 사용하는 언어의 경우도 싱글 스레드이지만 I/O Bound 작업에 대해서는 비동기 작업을 하면 성능이 향상됩니다.

다른 부분은 참고해도 문제 없을 것 같네요

2 Likes

아래 글 참고하시기 바랍니다.

ConfigureAwait in .NET 8

NET 8부터는 ConfigureAwait(false) 대신 ConfigureAwait(ConfigureAwaitOptions.None)와 같이 명확하게 할 수 있다는 말씀이신건가요~?

Async/Await - Best Practices in Asynchronous Programming

By Stephen Cleary | March 2013

좋은 자료 공유 감사드립니다!

UI가 없는 서버 환경에서 ConfigureAwait(false)는 성능 향상을 기대할 수 없다고 봐야 될 것

이 부분과 연결 된 참고할 만한 부분이 어딘지 제가 잘 못 찾겠습니다.. :sob:

링크 공유도 좋지만 간단하게나마 설명 부탁드리면 감사드리겠습니다!


그리고 주신 링크 읽어보니 재밌는 점을 발견했습니다.

Stephen Cleary

ConfigureAwait(false) is not a good way to avoid deadlocks. That’s not its purpose, and it’s a questionable solution at best.

이라고 말하고 있는데 .NET 문서에서는

Aside from performance, ConfigureAwait has another important aspect: It can avoid deadlocks.

라고 하고 있네요 ㅋㅋ 서로 상충 된 내용이 있네요.

1 Like

image

음…:thinking:

원 글의 저자에게서 Semaphore를 쓴 이유를 들을 수 있었습니다.

hi, semaphore is grate solution when you have limited resources and possible huge amount of actions.
For instance you have thousands ids and you need to perform some action for each in your db. With semaphore you can limit concurent actions to not overwhelming your db

제한된 자원(DB)이 있을 때 rate limit에 걸리게 하지 않기위해라고 합니다!
답변을 들으니 이해가 되긴 하네요.