동기 코드를 비동기 호출로 래핑


97

ASP.NET 응용 프로그램에 완료하는 데 많은 시간이 소요되는 메서드가 있습니다. 이 메소드에 대한 호출은 사용자가 제공하는 캐시 상태 및 매개 변수에 따라 한 사용자 요청 중에 최대 3 번 발생할 수 있습니다. 각 호출을 완료하는 데 약 1 ~ 2 초가 걸립니다. 메서드 자체는 서비스에 대한 동기 호출이며 구현을 재정의 할 가능성이 없습니다.
따라서 서비스에 대한 동기 호출은 다음과 같습니다.

public OutputModel Calculate(InputModel input)
{
    // do some stuff
    return Service.LongRunningCall(input);
}

그리고 메서드의 사용법은 다음과 같습니다 (메소드 호출이 두 번 이상 발생할 수 있음).

private void MakeRequest()
{
    // a lot of other stuff: preparing requests, sending/processing other requests, etc.
    var myOutput = Calculate(myInput);
    // stuff again
}

이 방법의 동시 작업을 제공하기 위해 내 측면에서 구현을 변경하려고 시도했으며 여기에 지금까지 온 것이 있습니다.

public async Task<OutputModel> CalculateAsync(InputModel input)
{
    return await Task.Run(() =>
    {
        return Calculate(input);
    });
}

사용법 ( "다른 작업 수행"코드의 일부는 서비스 호출과 동시에 실행 됨) :

private async Task MakeRequest()
{
    // do some stuff
    var task = CalculateAsync(myInput);
    // do other stuff
    var myOutput = await task;
    // some more stuff
}

내 질문은 다음과 같습니다. ASP.NET 응용 프로그램에서 실행 속도를 높이기 위해 올바른 접근 방식을 사용합니까? 아니면 동기 코드를 비동기 적으로 실행하려는 불필요한 작업을 수행하고 있습니까? 두 번째 접근 방식이 ASP.NET에서 옵션이 아닌 이유를 설명 할 수있는 사람이 있습니까 (정말 그렇지 않은 경우)? 또한 그러한 접근 방식이 적용 가능하다면, 현재 우리가 수행 할 수있는 유일한 호출 인 경우 이러한 메서드를 비동기 적으로 호출해야합니까 (완료를 기다리는 동안 수행 할 다른 작업이없는 경우)?
이 주제에 대한 인터넷의 대부분의 기사는 async-await이미 awaitable메소드를 제공하는 코드로 접근 방식을 사용 하는 방법을 다루지 만 제 경우는 아닙니다. 여기병렬 호출의 상황을 설명하지 않고 동기화 호출을 랩핑하는 옵션을 거부하는 내 사례를 설명하는 멋진 기사이지만 내 생각에는 내 상황이 바로 그 기회입니다.
도움과 팁에 미리 감사드립니다.

답변:


119

서로 다른 두 가지 유형의 동시성을 구별하는 것이 중요합니다. 비동기 동시성은 비행 중에 여러 비동기 작업이있는 경우입니다 (각 작업이 비동기이므로 실제로 스레드를 사용하지 않는 작업 ). 병렬 동시성은 각각 별도의 작업을 수행하는 여러 스레드가있는 경우입니다.

가장 먼저 할 일은이 가정을 재평가하는 것입니다.

메서드 자체는 서비스에 대한 동기 호출이며 구현을 재정의 할 가능성이 없습니다.

"서비스"가 서비스 또는 I / O 바운드 인 경우 최상의 솔루션은이를위한 비동기 API를 작성하는 것입니다.

"서비스"가 웹 서버와 동일한 시스템에서 실행되어야하는 CPU 바운드 작업이라는 가정을 계속하겠습니다.

그렇다면 다음으로 평가할 것은 또 다른 가정입니다.

더 빨리 실행하려면 요청이 필요합니다.

그게 당신이해야 할 일이라고 확신합니까? 대신 수행 할 수있는 프런트 엔드 변경이 있습니까? 예를 들어 요청을 시작하고 처리하는 동안 사용자가 다른 작업을 수행하도록 허용합니까?

예, 개별 요청을 더 빠르게 실행해야한다는 가정을 계속하겠습니다.

이 경우 웹 서버에서 병렬 코드를 실행해야합니다. 병렬 코드는 ASP.NET이 다른 요청을 처리하는 데 필요할 수있는 스레드를 사용하고 스레드를 제거 / 추가하면 ASP.NET 스레드 풀 휴리스틱이 해제되므로 일반적으로 권장되지 않습니다. 따라서이 결정은 전체 서버에 영향을 미칩니다.

ASP.NET에서 병렬 코드를 사용하는 것은 웹앱의 확장 성을 실제로 제한하기로 결정하는 것입니다. 특히 요청이 폭주하는 경우 상당한 양의 스레드 이탈을 볼 수 있습니다. 동시 사용자 수가 상당히 적다는 것을 알고 있는 경우 (즉, 공용 서버가 아님) ASP.NET에서만 병렬 코드를 사용하는 것이 좋습니다 .

따라서이 정도까지 도달하고 ASP.NET에서 병렬 처리를 수행하려는 경우 몇 가지 옵션이 있습니다.

더 쉬운 방법 중 하나는 Task.Run기존 코드와 매우 유사한를 사용하는 것입니다. 그러나 CalculateAsync처리가 비동기적임을 의미하므로 메서드를 구현하지 않는 것이 좋습니다 . 대신 Task.Run호출 지점에서 사용 하십시오.

private async Task MakeRequest()
{
  // do some stuff
  var task = Task.Run(() => Calculate(myInput));
  // do other stuff
  var myOutput = await task;
  // some more stuff
}

그것은 당신의 코드와 잘 작동하는 경우 또는, 당신은 사용할 수있는 Parallel유형, 즉, Parallel.For, Parallel.ForEach, 또는 Parallel.Invoke. Parallel코드 의 장점 은 요청 스레드가 병렬 스레드 중 하나로 사용 된 다음 스레드 컨텍스트에서 실행을 재개한다는 것입니다 ( async예제 보다 컨텍스트 전환이 적음 ).

private void MakeRequest()
{
  Parallel.Invoke(() => Calculate(myInput1),
      () => Calculate(myInput2),
      () => Calculate(myInput3));
}

ASP.NET에서 PLINQ (Parallel LINQ)를 사용하지 않는 것이 좋습니다.


1
자세한 답변에 감사드립니다. 한 가지 더 명확히하고 싶습니다. 를 사용하여 실행을 시작 Task.Run하면 콘텐츠가 스레드 풀에서 가져온 다른 스레드에서 실행됩니다. 그러면 블로킹 호출 (예 : 서비스에 대한 광산 호출)을 Runs 로 래핑 할 필요가 없습니다 . 왜냐하면 메서드 실행 중에 차단되는 스레드를 항상 하나씩 사용하기 때문입니다. 이러한 상황 async-await에서 내 경우 남은 유일한 이점 은 한 번에 여러 작업을 수행하는 것입니다. 내가 틀렸다면 정정 해주세요.
Eadel 2014 년

2
예, Run(또는 Parallel) 에서 얻는 유일한 이점 은 동시성입니다. 작업은 여전히 ​​각각 스레드를 차단하고 있습니다. 서비스가 웹 서비스라고 말 했으므로 Run또는 사용하지 않는 것이 좋습니다 Parallel. 대신 서비스에 대한 비동기 API를 작성하십시오.
Stephen Cleary 2014 년

3
@StephenCleary. "... 각 작업이 비동기식이므로 실제로 스레드를 사용하는 작업이 없습니다 ..."가 의미하는 바입니다. 감사합니다!
alltej

3
@alltej : 진정한 비동기 코드의 경우 스레드가 없습니다 .
Stephen Cleary

4
@JoshuaFrank : 직접적으로는 아닙니다. 동기 API 뒤에있는 코드는 정의에 따라 호출 스레드를 차단해야합니다. 당신은 할 수 와 같은 비동기 API를 사용하여 BeginGetResponse참조 - 그들 모두가 완료하지만, 이런 종류의 접근법이 까다 롭습니다 때까지 (동기) 블록 다음 그 중 몇 가지 시작하고 blog.stephencleary.com/2012/07/dont-block-on -async-code.htmlmsdn.microsoft.com/en-us/magazine/mt238404.aspx . async가능하다면 완전히 채택 하는 것이 일반적으로 더 쉽고 깔끔 합니다.
Stephen Cleary 2018

1

다음 코드는 Task를 항상 비동기 적으로 실행하도록 변환 할 수 있습니다.

private static async Task<T> ForceAsync<T>(Func<Task<T>> func)
{
    await Task.Yield();
    return await func();
}

그리고 나는 그것을 다음과 같은 방식으로 사용했습니다.

await ForceAsync(() => AsyncTaskWithNoAwaits())

그러면 모든 태스크가 비동기 적으로 실행되므로 WhenAll, WhenAny 시나리오 및 기타 용도로 결합 할 수 있습니다.

호출 된 코드의 첫 번째 줄로 Task.Yield ()를 추가 할 수도 있습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.