기본적으로 모든 기능이 비동기 적이 어서는 안되는 이유는 무엇입니까?


107

.net 4.5 의 async-await 패턴은 패러다임이 바뀌고 있습니다. 사실이 되기에는 너무 좋습니다.

차단이 과거의 일이기 때문에 IO가 많은 코드를 async-await로 이식했습니다.

꽤 많은 사람들이 async-await를 좀비 감염과 비교하고 있는데 나는 그것이 다소 정확하다는 것을 알았습니다. 비동기 코드는 다른 비동기 코드를 좋아합니다 (비동기 함수를 기다리려면 비동기 함수가 필요합니다). 따라서 점점 더 많은 함수가 비 동기화되고 이는 코드베이스에서 계속 증가합니다.

기능을 비동기로 변경하는 것은 다소 반복적이고 상상력이없는 작업입니다. async선언에 키워드를 던지고 반환 값을 래핑하면 Task<>거의 완료됩니다. 전체 프로세스가 얼마나 쉬운지는 다소 불안하며 곧 텍스트 대체 스크립트가 대부분의 "이동"을 자동화 할 것입니다.

그리고 이제 질문 .. 내 모든 코드가 천천히 비 동기화되는 경우 기본적으로 모든 코드를 비동기로 설정하지 않는 이유는 무엇입니까?

내가 생각하는 명백한 이유는 성능입니다. Async-await에는 비동기 일 필요가없는 오버 헤드와 코드가 있으며, 가급적이면 안됩니다. 그러나 성능이 유일한 문제인 경우 일부 영리한 최적화는 필요하지 않을 때 자동으로 오버 헤드를 제거 할 수 있습니다. "빠른 경로" 최적화 에 대해 읽었으며 , 그것만으로도 대부분을 처리해야하는 것 같습니다.

아마도 이것은 가비지 컬렉터가 가져온 패러다임 전환과 비슷할 것입니다. GC 초기에는 자신의 메모리를 확보하는 것이 확실히 더 효율적이었습니다. 그러나 대중은 여전히 ​​덜 효율적일 수있는 더 안전하고 단순한 코드를 선호하는 자동 수집을 선택했습니다 (더 이상 사실이 아닙니다). 아마도 이것이 여기에 해당되어야할까요? 모든 기능이 비동기 적이 어서는 안되는 이유는 무엇입니까?


8
지도 표시에 대한 크레딧을 C # 팀에 제공합니다. 수백 년 전에했던 것처럼 "용이 여기에있다". 당신은 배를 장비하고 거기로 갈 수 있으며, 당신은 태양이 빛나고 바람이 등에 불 때 살아남을 것입니다. 그리고 때때로 그들은 돌아 오지 않았습니다. async / await와 매우 유사하게 SO는지도에서 어떻게 나왔는지 이해하지 못한 사용자의 질문으로 가득 차 있습니다. 꽤 좋은 경고를 받았지만. 이제 C # 팀의 문제가 아니라 그들의 문제입니다. 그들은 용을 표시했습니다.
Hans Passant 2013-08-28

1
@Sayse 동기화 함수와 비동기 함수 사이의 구분을 제거하더라도 동 기적으로 구현 된 함수 호출은 여전히 ​​동기식입니다 (WriteLine 예제처럼)
talkol

2
당신의 방법이 async(일부 계약을 이행하기 위해) 필요하지 않는 한 , 이것은 아마도 나쁜 생각 일 것입니다. 비동기 (메소드 호출 비용 증가 await, 호출 코드에서 사용해야 함)의 단점은 있지만 장점은 없습니다.
svick

1
이것은 정말 흥미로운 질문이지만 아마도 그렇게 적합하지 않을 수도 있습니다. 프로그래머에게 마이그레이션하는 것을 고려할 수 있습니다.
Eric Lippert

1
내가 뭔가 빠졌을 수도 있지만 정확히 같은 질문이 있습니다. async / await가 항상 쌍으로 나오고 코드가 여전히 실행이 완료 될 때까지 기다려야하는 경우 기존 .NET 프레임 워크를 업데이트하고 추가 키워드를 구성하지 않고 기본적으로 비 동기화해야하는 메서드를 비동기로 만드는 것은 어떨까요? 언어는 이미 탈출하기 위해 고안된 키워드 스파게티로 변하고 있습니다. 나는 그들이 "var"가 제안 된 후에 그 일을 중단 했어야한다고 생각한다. 이제 우리는 "dynamic", asyn / await ... etc ... .NET-ify javascript를 사용하지 않는 이유는 무엇입니까? ;))
monstro

답변:


127

먼저 친절한 말씀 감사합니다. 정말 멋진 기능이며 그 일부가 된 것이 기쁩니다.

내 모든 코드가 천천히 비 동기화되는 경우 기본적으로 모든 코드를 비 동기화하지 않는 이유는 무엇입니까?

글쎄, 당신은 과장하고 있습니다. 모든 코드가 비동기로 바뀌지 않습니다. 두 개의 "일반"정수를 더하면 결과를 기다리지 않습니다. 미래 의 세 번째 정수 를 얻기 위해 두 개의 미래 정수 를 더할 때 - 그것이 바로 미래에 접근하게 될 정수 이기 때문입니다 Task<int>-물론 결과를 기다리고있을 것입니다.

모든 것을 비 동기화하지 않는 주된 이유 는 async / await의 목적이 대기 시간이 긴 작업이 많은 세계에서 코드를 더 쉽게 작성할 수 있도록하기 때문 입니다. 대부분의 작업은 지연 시간이 길지 않으므로 지연 시간을 완화하는 성능 저하를 감수하는 것은 의미가 없습니다. 오히려 주요 작업은 대기 시간이 길고 이러한 작업으로 인해 코드 전체에서 좀비가 비 동기화됩니다.

성능이 유일한 문제인 경우 일부 영리한 최적화는 필요하지 않을 때 자동으로 오버 헤드를 제거 할 수 있습니다.

이론적으로 이론과 실제는 비슷합니다. 실제로는 그렇지 않습니다.

이런 종류의 변환과 최적화 과정에 대한 세 가지 점을 알려 드리겠습니다.

첫 번째 요점은 C # / VB / F #의 비동기는 본질적으로 제한된 형태의 연속 전달 입니다. 함수형 언어 커뮤니티에서 엄청난 양의 연구를 통해 연속 전달 스타일을 많이 사용하는 코드를 최적화하는 방법을 파악하는 방법을 알아 냈습니다. 컴파일러 팀은 "비동기"가 기본값이고 비 비동기 메서드를 식별하고 비 동기화해야하는 세계에서 매우 유사한 문제를 해결해야 할 것입니다. C # 팀은 공개 된 연구 문제를 다루는 데별로 관심이 없으므로 바로 거기에 대한 큰 포인트입니다.

반대의 두 번째 요점은 C #에는 이러한 종류의 최적화를보다 다루기 쉽게 만드는 "참조 투명성"수준이 없다는 것입니다. "참조 투명성"이란 식의 값이 평가 될 때 의존하지 않는 속성을 의미합니다 . 다음과 같은 표현식 2 + 2은 참조 적으로 투명합니다. 원하는 경우 컴파일 타임에 평가를 수행하거나 런타임까지 연기하고 동일한 답변을 얻을 수 있습니다. 그러나 x와 y가 시간이 지남에 따라 변할 수x+y 있기 때문에 다음 과 같은 표현식 은 시간에 따라 움직일 수 없습니다 .

비동기를 사용하면 부작용이 언제 발생하는지 추론하기가 훨씬 더 어려워집니다. 비동기 전에 다음과 같이 말한 경우 :

M();
N();

M()했다 void M() { Q(); R(); }, 그리고 N()이었다 void N() { S(); T(); }, 그리고 RS생산 부작용, 당신은 R의 부작용 S의 부작용 전에 일어나는 것을 알고있다. 그러나 만약 async void M() { await Q(); R(); }그렇다면 갑자기 그것은 창 밖으로 나갑니다. (물론 기다리지 않는 한, R()그 전후에 일어날 것인지 보장 할 수 없습니다 . 물론 그 이후까지 기다릴 필요는 없습니다 .)S()M()TaskN()

이제 어떤 순서의 부작용이 발생하는지 더 이상 알지 못하는 이 속성이 최적화 프로그램이 비 동기화 해제를 관리하는 코드를 제외하고 프로그램의 모든 코드에 적용 된다고 상상해보십시오 . 기본적으로 어떤식이 어떤 순서로 평가되는지 더 이상 알 수 없습니다. 즉, 모든식이 참조 적으로 투명해야하므로 C #과 같은 언어에서는 어렵습니다.

반대의 세 번째 요점은 "비동기가 왜 그렇게 특별한가?"라는 질문을해야한다는 것입니다. 모든 작업이 실제로 이루어져야한다고 주장 Task<T>하려면 "왜 안 Lazy<T>되는가?"라는 질문에 답할 수 있어야합니다. 또는 "왜 안돼 Nullable<T>?" 또는 "왜 안돼 IEnumerable<T>?" 우리는 그렇게 쉽게 할 수 있기 때문입니다. 모든 작업이 nullable로 해제 되는 경우가 아닌 이유는 무엇 입니까? 또는 모든 작업이 느리게 계산되고 결과가 나중에 캐시 되거나 모든 작업의 ​​결과가 단일 값이 아닌 일련의 값 입니다. 그런 다음 "아, 이것은 null이 아니어야합니다. 따라서 더 나은 코드를 생성 할 수 있습니다"등을 알고있는 상황을 최적화해야합니다.

요점은 Task<T>이 정도의 작업을 보증 하는 것이 실제로 그렇게 특별 하다는 것이 나에게 명확하지 않다는 것입니다.

이러한 종류에 관심이 있다면 참조 투명성이 훨씬 더 강하고 모든 종류의 비 순차적 평가를 허용하고 자동 캐싱을 수행하는 Haskell과 같은 기능 언어를 조사하는 것이 좋습니다. Haskell은 또한 내가 언급 한 "모나드 리프팅"의 종류에 대한 유형 시스템에서 훨씬 더 강력한 지원을 제공합니다.


await없이 호출되는 비동기 함수를 갖는 것은 나에게 의미가 없습니다 (일반적인 경우). 이 기능을 제거하면 컴파일러는 함수가 비동기인지 아닌지 스스로 결정할 수 있습니다 (await를 호출합니까?). 그런 다음 두 경우 (비동기 및 동기화)에 대해 동일한 구문을 가질 수 있으며 호출에서 await 만 차별화 요소로 사용할 수 있습니다. 좀비 감염은 :) 해결
talkol을

귀하의 요청에 따라 프로그래머와 토론을 계속했습니다. programmers.stackexchange.com/questions/209872/…
talkol

@EricLippert-언제나처럼 아주 좋은 대답 :) "높은 대기 시간"을 명확히 할 수 있는지 궁금 했습니까? 여기에 밀리 초 단위의 일반적인 범위가 있습니까? 나는 그것을 남용하고 싶지 않기 때문에 비동기를 사용하기위한 하한선이 어디에 있는지 알아 내려고 노력하고 있습니다.
Travis J

7
@TravisJ : 지침 : UI 스레드를 30ms 이상 차단하지 마십시오. 그 이상은 사용자가 일시 중지를 눈에 띄게 할 위험이 있습니다.
Eric Lippert 2013

1
저에게 도전적인 것은 무언가가 동 기적으로 또는 비동기 적으로 수행되는지 여부가 변경 될 수있는 구현 세부 사항이라는 것입니다. 그러나 해당 구현의 변경은이를 호출하는 코드,이를 호출하는 코드 등을 통해 파급 효과를 가져옵니다. 우리는 코드가 의존하는 코드로 인해 결국 코드를 ​​변경하게되는데, 이는 일반적으로 피해야 할 일입니다. 또는 async/await추상화 계층 아래에 ​​숨겨진 것이 비동기적일 있기 때문에 사용 합니다.
Scott Hannen

23

모든 기능이 비동기 적이 어서는 안되는 이유는 무엇입니까?

언급했듯이 성능이 한 가지 이유입니다. 연결 한 "빠른 경로"옵션은 완료된 작업의 경우 성능을 향상 시키지만 단일 메서드 호출에 비해 여전히 훨씬 더 많은 지침과 오버 헤드가 필요합니다. 따라서 "빠른 경로"가있는 경우에도 각 비동기 메서드 호출에 많은 복잡성과 오버 헤드가 추가됩니다.

이전 버전과의 호환성 및 다른 언어 (interop 시나리오 포함)와의 호환성도 문제가 될 수 있습니다.

다른 하나는 복잡성과 의도의 문제입니다. 비동기 작업은 복잡성을 추가합니다. 대부분의 경우 언어 기능이이를 숨기지 만 메서드를 만드는 방법이 사용에 async확실히 복잡성을 추가 하는 경우가 많습니다 . 비동기 메서드가 예기치 않은 스레딩 문제를 쉽게 일으킬 수 있으므로 동기화 컨텍스트가없는 경우 특히 그렇습니다.

또한 본질적으로 비동기 적이 지 않은 루틴이 많이 있습니다. 동기 작업으로 더 의미가 있습니다. 예를 들어, 비동기식 일 이유가 전혀 없기 때문에 강제 Math.Sqrt하는 것은 Task<double> Math.SqrtAsync우스꽝 스러울 것입니다. async애플리케이션 을 푸시 하는 대신 모든 곳에await 전파하게 됩니다.

이것은 또한 현재의 패러다임을 완전히 깨뜨릴뿐만 아니라 속성에 문제를 일으킬뿐만 아니라 (효과적으로 메소드 쌍입니다 .. 그것들도 비 동기화 될까요?) 프레임 워크와 언어의 설계 전반에 걸쳐 다른 영향을 미칠 것입니다.

많은 IO 바인딩 작업을 수행하는 경우 다음을 사용하는 경향이 있습니다. async 퍼베이시브 이 큰 추가 사항이며 많은 루틴이 async. 당신은 일반적으로 CPU 바운드 작업을, 일을 시작할 때 그러나, 일을하는 것은 async실제로는 좋지 않다 - 당신이 있다는 API에서 CPU 사이클을 사용하고 있다는 사실을 숨기고 나타납니다가 비동기로하지만, 정말 반드시 진정으로 비동기되지 않습니다.


정확히 내가 작성하려고했던 것 (성능), 이전 버전과의 호환성은 다른 것일 수 있습니다. dll은 비동기 / 대기도 지원하지 않는 이전 언어와 함께 사용됩니다
Sayse

우리는 단순히 동기 및 비동기 기능 사이의 차이를 제거하면 만들기 SQRT 비동기 말도하지 않습니다
talkol

@talkol 나는이 주위를 회전 것 같아요 - 왜 해야 비동기의 복잡성의 모든 함수 호출 걸릴?
Reed Copsey

2
@talkol 나는 반드시 사실이 아니다 주장 것 - 비동기 차단보다 더 나쁜 것을 버그 자체를 추가 할 수 있습니다 ..
리드 Copsey

1
@talkol await FooAsync()보다 간단한 방법은 Foo()무엇입니까? 그리고 때때로 작은 도미노 효과 대신에, 당신은 항상 거대한 도미노 효과를 가지고 있으며 그것을 개선이라고 부릅니까?
svick

4

성능 제쳐두고-비동기는 생산성 비용을 가질 수 있습니다. 클라이언트 (WinForms, WPF, Windows Phone)에서는 생산성에 도움이됩니다. 그러나 서버 또는 기타 비 UI 시나리오에서는 생산성 을 지불 합니다. 기본적으로 비동기식으로 가고 싶지는 않습니다. 확장 성 이점이 필요할 때 사용하십시오.

스위트 스팟에서 사용하십시오. 다른 경우에는하지 마십시오.


2
+1-임의의 완료 순서와 함께 5 개의 비동기 작업을 병렬로 실행하는 코드의 멘탈 맵을 만드는 간단한 시도는 대부분의 사람들에게 하루 동안 충분한 고통을 제공합니다. 코드가 더 힘들어 좋은 오래된 동기 코드보다 비동기의 행동을 추론 (따라서 본질적으로 평행) ...
알렉세이 Levenkov

2

확장 성이 필요하지 않은 경우 모든 메서드를 비 동기화해야하는 좋은 이유가 있다고 생각합니다. 선택적 메서드 비 동기화는 코드가 진화하지 않고 메서드 A ()가 항상 CPU 바인딩 (동기화 유지)이고 메서드 B ()가 항상 I / O 바인딩 (비동기 표시)임을 알고있는 경우에만 작동합니다.

하지만 상황이 바뀌면 어떨까요? 예, A ()는 계산을 수행하고 있지만 향후 언젠가 거기에 로깅,보고 또는 예측할 수없는 구현이있는 사용자 정의 콜백을 추가해야했습니다. 또는 알고리즘이 확장되어 이제 CPU 계산뿐만 아니라 포함됩니다. 일부 I / O? 메서드를 비동기로 변환해야하지만 이로 인해 API가 중단되고 스택의 모든 호출자도 업데이트해야합니다 (다른 공급 업체의 다른 앱일 수도 있음). 또는 동기화 버전과 함께 비동기 버전을 추가해야하지만 이것은 큰 차이가 없습니다. 동기화 버전을 사용하면 차단되므로 거의 허용되지 않습니다.

API를 변경하지 않고 기존 동기화 방법을 비 동기화 할 수 있다면 좋을 것입니다. 그러나 실제로는 그러한 옵션이 없습니다. 현재 필요하지 않더라도 비동기 버전을 사용하는 것이 향후 호환성 문제가 발생하지 않도록 보장하는 유일한 방법입니다.


이것은 극단적 인 코멘트처럼 보이지만, 많은 진실이 있습니다. 첫째,이 문제로 인해 : "이것은 API를 깨뜨리고 모든 호출자가 스택에 올라갑니다"async / await는 응용 프로그램을 더 긴밀하게 결합시킵니다. 예를 들어 하위 클래스가 async / await를 사용하려는 경우 Liskov Substitution 원칙을 쉽게 위반할 수 있습니다. 또한 대부분의 메서드가 비동기 / 대기 (await)가 필요하지 않은 마이크로 서비스 아키텍처를 상상하기 어렵습니다.
ItsAllABadJoke
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.