C #에서 async / await 사용 지침이 우수한 아키텍처 및 추상화 계층 개념과 모순되지 않습니까?


103

이 질문은 C # 언어와 관련이 있지만 Java 또는 TypeScript와 같은 다른 언어를 다루기를 기대합니다.

.NET에서 비동기 호출 사용에 대한 모범 사례 를 권장 합니다. 이러한 권장 사항 중에서 두 가지를 선택하십시오.

  • 비동기 메소드의 서명을 변경하여 Task 또는 Task <>를 반환하십시오 (TypeScript에서는 Promise <>).
  • xxxAsync ()로 끝나도록 비동기 메소드의 이름을 변경하십시오.

이제 하위 수준의 동기 구성 요소를 비동기 구성 요소로 교체하면 응용 프로그램의 전체 스택에 영향을줍니다. async / await는 "완벽하게"사용될 경우에만 긍정적 인 영향을 미치므로 응용 프로그램에있는 모든 계층의 서명 및 메서드 이름을 변경해야합니다.

좋은 아키텍처는 종종 각 계층 사이에 추상화를 배치하여 하위 수준의 구성 요소를 다른 구성 요소로 대체하는 것이 상위 구성 요소에 의해 보이지 않게합니다. C #에서 추상화는 인터페이스 형식을 취합니다. 새로운 저수준의 비동기 구성 요소를 도입하는 경우 호출 스택의 각 인터페이스를 수정하거나 새 인터페이스로 교체해야합니다. 구현 클래스에서 문제가 해결되는 방식 (비동기 또는 동기화)이 더 이상 호출자에게 숨겨지지 않습니다. 발신자는 동기화 또는 비동기인지 알아야합니다.

"좋은 아키텍처"원칙과 모순되는 모범 사례를 비 동기화 / 기다리지 않습니까?

각 인터페이스 (IEnumerable, IDataAccessLayer)는 비동기 종속성으로 전환 할 때 스택에서 대체 될 수 있도록 비동기 대응 물 (IAsyncEnumerable, IAsyncDataAccessLayer)이 필요합니까?

우리가 문제를 조금 더 추진한다면, 모든 메소드가 비동기 (태스크 <> 또는 Promise <>를 리턴하기 위해)라고 가정하고 실제로 비동기 호출을 동기화하지 않는 메소드를 가정하는 것이 더 간단하지 않을 것입니다 비동기? 이것은 미래의 프로그래밍 언어에서 예상되는 것입니까?


5
이것은 훌륭한 토론 질문처럼 들리지만, 이것이 여기에 대답하기에는 너무 의견에 근거한 것 같습니다.
Euphoric

22
@Euphoric : 여기에서 가져온 문제는 C # 지침보다 더 깊다고 생각합니다. 응용 프로그램의 일부를 비동기 동작으로 변경하면 전체 시스템에 로컬 영향을 미치지 않을 수 있다는 사실에 불과합니다. 그래서 내 직감은 기술적 사실에 근거하여 이것에 대한 비 의견에 대한 답변이 있어야한다고 말합니다. 그러므로 나는 여기있는 모든 사람들이이 질문을 너무 일찍 끝내지 말 것을 권한다. 대신에 어떤 답변이 올 것인지 기다린다.
Doc Brown

25
@DocBrown 여기서 더 깊은 질문은 "시스템의 일부를 변경해야 할 부분 없이도 동기에서 비동기로 변경할 수 있습니까?"라고 생각합니다. 그 대답은 분명 "아니오"라고 생각합니다. 이 경우 "좋은 아키텍처 및 계층화 개념"이 어떻게 적용되는지 알 수 없습니다.
Euphoric

6
@Euphoric : 비 소위 답변에 대한 좋은 근거처럼 들린다 ;-)
Doc Brown

5
@ Gherman : 많은 언어와 마찬가지로 C #은 반환 유형만으로 오버로드를 수행 할 수 없기 때문에. 동기화 상대방과 동일한 서명을 가진 비동기 메소드로 끝날 것입니다 CancellationToken. 기존 동기화 방법을 제거하고 모든 코드를 사전에 차단하는 것은 명백한 시작이 아닙니다.
Jeroen Mostert

답변:


111

당신의 기능은 어떤 색입니까?

당신은 Bob Nystrom의 What Color Is Your Function 1에 관심이있을 것 입니다.

이 기사에서는 다음과 같은 가상의 언어를 설명합니다.

  • 각 기능의 색상은 파란색 또는 빨간색입니다.
  • 빨간색 기능은 파란색 또는 빨간색 기능을 호출 할 수 있으며 문제 없습니다.
  • 파란색 함수는 파란색 함수 만 호출 할 수 있습니다.

허구이지만, 이것은 프로그래밍 언어에서 상당히 정기적으로 발생합니다.

  • C ++에서 "const"메소드는 다른 "const"메소드 만 호출 할 수 있습니다 this.
  • Haskell에서 비 IO 함수는 비 IO 함수 만 호출 할 수 있습니다.
  • C #에서 동기화 기능은 동기화 기능 2 만 호출 할 수 있습니다 .

아시다시피, 이러한 규칙으로 인해 빨간색 함수 가 코드 기반 에 퍼지는 경향이 있습니다. 하나를 삽입하면 조금씩 전체 코드베이스를 식민지화합니다.

1 밥 니 스트롬은 따로 블로그에서, 또한 다트 팀의 일부이며이 작은 크래프팅 통역 시리즈를 작성했습니다; 모든 프로그래밍 언어 / 컴파일러에 권장됩니다.

2 비동기 함수를 호출하고 반환 될 때까지 차단 할 수 있기 때문에 사실은 아니지만 ...

언어 제한

이것은 본질적으로 언어 / 런타임 제한입니다.

예를 들어 Erlang 및 Go와 같은 M : N 스레딩이 포함 된 언어에는 async기능 이 없습니다 . 각 기능은 잠재적으로 비동기 적이며 "준비"기능은 일시 중지, 교체 및 다시 준비되면 다시 교체됩니다.

C #은 1 : 1 스레딩 모델을 사용했기 때문에 실수로 스레드를 차단하지 않기 위해 언어의 동기 성을 표시하기로 결정했습니다.

언어 제한이있는 경우 코딩 지침을 따라야합니다.


4
IO 함수는 확산되는 경향이 있지만 근면성을 통해 대부분 코드의 진입 점 근처 (호출시 스택 내)의 함수로 분리 할 수 ​​있습니다. 해당 함수가 IO 함수를 호출 한 다음 다른 함수가 해당 출력을 처리하고 추가 IO에 필요한 결과를 리턴하도록함으로써이를 수행 할 수 있습니다. 이 스타일을 사용하면 코드 기반을보다 쉽게 ​​관리하고 작업 할 수 있습니다. 나는 동기와 일치가 있는지 궁금합니다.
jpmc26

16
"M : N"및 "1 : 1"스레딩은 무엇을 의미합니까?
Captain Man

14
@CaptainMan : 1 : 1 스레딩은 하나의 응용 프로그램 스레드를 하나의 OS 스레드에 매핑하는 것을 의미합니다. 이는 C, C ++, Java 또는 C #과 같은 언어의 경우입니다. 반대로 M : N 스레딩은 M 개의 응용 프로그램 스레드를 N 개의 OS 스레드에 매핑하는 것을 의미합니다. Go의 경우 응용 프로그램 스레드를 "goroutine"이라고하고 Erlang의 경우 "actor"라고하며 "green thread"또는 "fibers"라고 들었을 수도 있습니다. 병렬 처리를 요구하지 않고 동시성을 제공합니다. 불행히도이 주제에 관한 Wikipedia 기사 는 다소 드물다.
Matthieu M.

2
이것은 다소 관련이 있지만이 "함수 색상"아이디어는 사용자의 입력을 차단하는 기능 (예 : 모달 대화 상자, 메시지 상자, 일부 콘솔 I / O 형식 등)에도 적용됩니다. 프레임 워크는 처음부터있었습니다.
jrh

2
@MatthieuM. C #에는 하나의 OS 스레드 당 하나의 응용 프로그램 스레드가 없으며 결코 없습니다. 이것은 네이티브 코드와 상호 작용할 때 매우 분명하며 특히 MS SQL에서 실행할 때 분명합니다. 물론, 협동적인 일과는 항상 가능했고 (보다 간단하다 async); 실제로 반응 형 UI를 구축하는 데 매우 일반적인 패턴이었습니다. 얼랭만큼 예뻤나요? 아니. 그러나 그것은 여전히 ​​C와는 거리가 멀다 :)
Luaan

82

여기에 모순이 있지만 "모범 사례"가 나쁜 것은 아닙니다. 비동기 함수는 동기 함수와 본질적으로 다른 것이기 때문입니다. 종속성 (일반적으로 일부 IO)의 결과를 기다리는 대신 기본 이벤트 루프에서 처리 할 작업을 만듭니다. 이것은 추상화에서 잘 숨겨 질 수있는 차이가 아닙니다.


27
답은이 IMO만큼 간단합니다. 동기 프로세스와 비동기 프로세스의 차이점은 구현 세부 사항이 아니라 의미 상 다른 계약입니다.
Ant P

11
@ AntP : 나는 그것이 간단하다는 데 동의하지 않습니다; 예를 들어 C # 언어로 표시되지만 Go 언어로는 표시되지 않습니다. 따라서 이것은 비동기 프로세스의 고유 속성이 아니며, 주어진 언어로 비동기 프로세스가 모델링되는 방식의 문제입니다.
Matthieu M.

1
@MatthieuM. 예. 그러나 asyncC #에서 메소드를 사용 하여 원하는 경우 동기 계약도 제공 할 수 있습니다 . 유일한 차이점은 Go는 기본적으로 비동기이고 C #은 기본적으로 동기라는 점입니다. async두 번째 프로그래밍 모델을 제공합니다- async 추상화입니다 (실제로 수행되는 작업은 런타임, 작업 스케줄러, 동기화 컨텍스트, 대기자 구현에 따라 다릅니다 ...).
Luaan

6

비동기 메소드는 알고있는 것처럼 동기 방식과 다르게 작동합니다. 런타임에 비동기 호출을 동기 호출로 변환하는 것은 쉽지 않지만 그 반대는 말할 수 없습니다. 그렇다면 논리는 왜 필요한지 모든 메소드의 비동기 메소드를 작성하고 호출자가 동기 메소드로 필요에 따라 "변환"하지 않는 이유는 무엇입니까?

어떤 의미에서는 예외를 던지는 방법과 "안전한"오류가 발생하더라도 던지지 않는 방법을 갖는 것과 같습니다. 코더는 어떤 방법으로 다른 방법으로 변환 할 수있는 이러한 방법을 제공하기에 과도합니까?

여기에는 두 가지 생각의 학교가 있습니다. 하나는 여러 가지 방법을 만드는 것입니다. 각 방법은 선택적인 매개 변수를 제공하거나 비동기와 같은 동작에 사소한 변경을 제공 할 수있는 다른 방법을 개별적으로 호출하는 것입니다. 다른 하나는 인터페이스 메소드를 최소화하여 필수 수정 사항을 베어러가 자신에게 필요한 수정 작업을 수행 할 수 있도록하는 것입니다.

첫 번째 학교에 다니는 경우 모든 통화가 배가되는 것을 피하기 위해 동기식 및 비동기식 통화에 클래스를 전용으로 사용하는 논리가 있습니다. Microsoft는이 사고 방식을 선호하는 경향이 있으며, 일반적으로 Microsoft가 선호하는 스타일과 일관성을 유지하려면 인터페이스가 거의 항상 "I"로 시작하는 것과 같은 방식으로 비동기 버전을 사용해야합니다. "올바른 방법"보다는 프로젝트에 일관된 스타일을 유지하고 프로젝트에 추가 한 개발 스타일을 근본적으로 바꾸는 것이 더 낫기 때문에 그 자체가 잘못 이 아니라는 점을 강조하겠습니다 .

즉, 인터페이스 방법을 최소화하는 두 번째 학교를 선호하는 경향이 있습니다. 메소드가 비동기 방식으로 호출 될 수 있다고 생각하면 나를위한 메소드는 비동기식입니다. 호출자는 진행하기 전에 해당 작업이 완료 될 때까지 기다릴 지 여부를 결정할 수 있습니다. 이 인터페이스가 라이브러리에 대한 인터페이스 인 경우 더 이상 사용하지 않거나 조정해야하는 메소드 수를 최소화하기 위해이 방법을 사용하는 것이 더 합리적입니다. 인터페이스가 내 프로젝트에서 내부 용인 경우 제공된 매개 변수에 대해 "추가"방법이없는 경우 프로젝트 전체에서 필요한 모든 호출에 대한 방법을 추가합니다. 기존 방법으로.

그러나이 분야의 많은 것들과 마찬가지로 크게 주관적입니다. 두 방법 모두 장단점이 있습니다. 또한 Microsoft는 변수 이름의 시작 부분에 유형을 나타내는 문자를 추가하는 규칙을 시작했으며 "m_"은 변수 이름을 나타내는 변수 이름을 나타내는 멤버임을 나타냅니다 m_pUser. 내 요점은 Microsoft조차도 완벽하지 않으며 실수를 범할 수 있다는 것입니다.

즉, 프로젝트 가이 비동기 규칙을 따르는 경우 프로젝트를 존중하고 스타일을 계속하는 것이 좋습니다. 자신 만의 프로젝트를받은 후에 만 ​​가장 적합한 방식으로 프로젝트를 작성할 수 있습니다.


6
"런타임에서 비동기 호출을 동기 호출로 변환하는 것은 쉽지 않다"라고 정확히 확신하지 못한다. .NET에서 .Wait()method 등을 사용 하면 부정적인 결과를 초래할 수 있으며 js에서는 내가 아는 한 전혀 불가능합니다.
max630

2
@ max630 고려해야 할 동시적인 문제는 없지만 처음에 동기 작업이라면 교착 상태가 발생하지 않을 가능성이 있습니다. 즉, 사소한 것이 "동기화로 변환하려면 여기를 두 번 클릭하십시오"는 의미는 아닙니다. js에서는 Promise 인스턴스를 반환하고 resolve를 호출합니다.
Neil

2
네 엉덩이는 완전히 통증이 동기화 다시 비동기를 변환하는
이완

4
@Neil 자바 스크립트에서는 전화를 걸어 Promise.resolve(x)콜백을 추가 하더라도 해당 콜백은 즉시 실행되지 않습니다.
NickL

1
@Neil 인터페이스가 비동기 메소드를 노출하는 경우 Task를 대기해도 교착 상태가 발생하지 않는다고 예상하는 것은 좋은 가정이 아닙니다. 인터페이스가 최신 버전에서 변경 될 수있는 문서의 약속보다 실제로 메소드 서명에서 동기식임을 표시하는 것이 훨씬 좋습니다.
Carl Walsh

2

서명을 변경하지 않고 비동기 방식으로 함수를 호출 할 수있는 방법이 있다고 가정 해 봅시다.

정말 시원하고 아무도 이름을 변경하지 않는 것이 좋습니다.

그러나 실제 비동기 함수는 다른 비동기 함수를 기다리는 함수뿐만 아니라 가장 낮은 수준은 비동기 특성에 따라 특정 구조를 갖습니다. 예 :

public class HTTPClient
{
    public HTTPResponse GET()
    {
        //send data
        while(!timedOut)
        {
            //check for response
            if(response) { 
                this.GotResponse(response); 
            }
            this.YouCanWait();
        }
    }

    //tell calling code that they should watch for this event
    public EventHander GotResponse
    //indicate to calling code that they can go and do something else for a bit
    public EventHander YouCanWait;
}

그것은 코드가 비동기식으로 코드를 실행 Task하고 async캡슐화 하는 데 필요한 두 가지 정보입니다 .

비동기 함수를 수행하는 방법은 여러 가지가 있습니다. async Task리턴 유형을 통해 컴파일러에 내장 된 하나의 패턴이므로 이벤트를 수동으로 연결할 필요가 없습니다.


0

나는 C #이 덜하고 더 일반적인 방식으로 요점을 다룰 것입니다.

"좋은 아키텍처"원칙과 모순되는 모범 사례를 비 동기화 / 기다리지 않습니까?

API 디자인에서 선택한 내용과 사용자에게 제공하는 내용에 따라 다릅니다.

API의 하나의 함수 만 비동기식으로 만들려면 명명 규칙을 따르는 데 관심이 거의 없습니다. 항상 Task <> / Promise <> / Future <> / ...를 반환 유형으로 반환하면 자체 문서화됩니다. 동기화 응답을 원한다면 대기해도 여전히 그렇게 할 수 있지만 항상 그렇게하면 약간의 상용구가됩니다.

그러나 API 동기화 만 수행하는 경우 사용자가 비동기식을 원하면 자체 비동기식 부분을 관리해야합니다.

이를 통해 추가 작업을 많이 수행 할 수 있지만 동시에 허용되는 통화 수, 시간 초과, 재시도 횟수 등을 사용자에게 더 잘 제어 할 수 있습니다.

거대한 API를 사용하는 대규모 시스템에서 기본적으로 동기화되도록 대부분을 구현하는 것은 특히 리소스 (파일 시스템, CPU, 데이터베이스 등)를 공유하는 경우 API의 각 부분을 독립적으로 관리하는 것보다 쉽고 효율적일 수 있습니다.

실제로 가장 복잡한 부분의 경우 API의 동일한 부분에 대해 두 가지 구현을 완벽하게 수행 할 수 있습니다. 하나는 편리한 작업을 수행하는 동기 요소, 하나는 비동기 작업을 수행하는 비동기 작업은 작업을 처리하고 동시성,로드, 시간 초과 및 재시도 만 관리합니다. .

어쩌면 다른 시스템에 대한 경험이 없기 때문에 다른 사람이 자신의 경험을 공유 할 수 있습니다.


2
@Miral 두 가지 방법으로 "동기화 메소드에서 비 동기화 메소드 호출"을 사용했습니다.
Adrian Wragg

@AdrianWragg 그래서 나는; 내 뇌에는 경쟁 조건이 있었을 것입니다. 내가 고칠 게
Miral December

그것은 다른 길입니다. sync 메소드에서 비동기 메소드를 호출하는 것은 쉽지 않지만 async 메소드에서 sync 메소드를 호출하는 것은 불가능합니다. (그리고 상황이 완전히 붕괴되는 곳은 누군가 어쨌든 후자를 시도 할 때 교착 상태로 이어질 수 있습니다.) 하나를 선택해야 할 경우 기본적으로 비동기가 더 나은 선택입니다. 불행히도 비동기 구현은 비동기 메서드 만 호출 할 수 있기 때문에 더 어려운 선택이기도합니다.
Miral December

(그리고 이것에 의해 나는 물론 블로킹 동기화 방법을 의미합니다 . 당신은 비동기 메서드에서 동기식으로 순수한 CPU 바운드 계산을하는 것을 호출 할 수 있습니다. UI 컨텍스트가 아니라 잠금 또는 I / O 또는 다른 작업을 위해 유휴 대기하는 호출을 차단하는 것은 좋지 않습니다.)
Miral
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.