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

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

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

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

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

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

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

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



답변

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

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

글쎄, 당신은 과장하고 있습니다. 모든 코드가 비동기로 바뀌지 않습니다. 두 개의 “일반”정수를 더하면 결과를 기다리지 않습니다. 미래 의 세 번째 정수 를 얻기 위해 두 개의 미래 정수 를 더할 때 – 그것이 바로 미래에 접근하게 될 정수 이기 때문입니다 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은 또한 내가 언급 한 “모나드 리프팅”의 종류에 대한 유형 시스템에서 훨씬 더 강력한 지원을 제공합니다.


답변

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

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

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

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

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

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

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


답변

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

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


답변

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

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

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


답변