Task.Run을 올바르게 사용할 때와 async-await 만있는 경우


317

올바른 아키텍처에 대한 의견을 묻고 싶습니다 Task.Run. WPF .NET 4.5 응용 프로그램 (Caliburn Micro 프레임 워크 사용)에서 느린 UI가 발생합니다.

기본적으로 나는 (매우 단순화 된 코드 스 니펫) :

public class PageViewModel : IHandle<SomeMessage>
{
   ...

   public async void Handle(SomeMessage message)
   {
      ShowLoadingAnimation();

      // Makes UI very laggy, but still not dead
      await this.contentLoader.LoadContentAsync();

      HideLoadingAnimation();
   }
}

public class ContentLoader
{
    public async Task LoadContentAsync()
    {
        await DoCpuBoundWorkAsync();
        await DoIoBoundWorkAsync();
        await DoCpuBoundWorkAsync();

        // I am not really sure what all I can consider as CPU bound as slowing down the UI
        await DoSomeOtherWorkAsync();
    }
}

내가 읽거나 본 기사 / 비디오 await async에서 백그라운드 스레드에서 실행되고 있지 않고 백그라운드에서 작업을 시작하기 위해 대기 중으로 래핑해야한다는 것을 알고 있습니다 Task.Run(async () => ... ). 를 사용 async await하면 UI가 차단되지 않지만 여전히 UI 스레드에서 실행 중이므로 지연됩니다.

Task.Run을 놓을 가장 좋은 곳은 어디입니까?

그냥해야 할까

  1. .NET의 스레딩 작업이 적기 때문에 외부 호출을 래핑하십시오.

  2. 또는 내부에서 실행되는 CPU 바인딩 메소드 만 랩핑해야 Task.Run다른 장소에서 재사용 할 수 있습니까? 코어의 깊은 백그라운드 스레드에서 작업을 시작하는 것이 좋은 아이디어인지 확실하지 않습니다.

광고 (1)에서 첫 번째 해결책은 다음과 같습니다.

public async void Handle(SomeMessage message)
{
    ShowLoadingAnimation();
    await Task.Run(async () => await this.contentLoader.LoadContentAsync());
    HideLoadingAnimation();
}

// Other methods do not use Task.Run as everything regardless
// if I/O or CPU bound would now run in the background.

광고 (2), 두 번째 해결책은 다음과 같습니다.

public async Task DoCpuBoundWorkAsync()
{
    await Task.Run(() => {
        // Do lot of work here
    });
}

public async Task DoSomeOtherWorkAsync(
{
    // I am not sure how to handle this methods -
    // probably need to test one by one, if it is slowing down UI
}

BTW, (1)의 줄 await Task.Run(async () => await this.contentLoader.LoadContentAsync());은 단순히이어야 await Task.Run( () => this.contentLoader.LoadContentAsync() );합니다. AFAIK 당신은 초 awaitasync내부를 추가하여 아무것도 얻지 못합니다 Task.Run. 그리고 매개 변수를 전달하지 않기 때문에 약간 더 단순화됩니다 await Task.Run( this.contentLoader.LoadContentAsync );.
ToolmakerSteve

내부에 두 번째 기다림이 있으면 실제로 약간의 차이가 있습니다. 이 기사를 참조 하십시오 . 나는이 특별한 점으로 동의하지 않고 기다리고있는 대신 직접 작업을 반환하는 것을 선호합니다. (의견에서 제안한대로)
Lukas K

답변:


365

내 블로그에 수집 된 UI 스레드 작업을 수행하기위한 지침을 참고하십시오 .

  • 한 번에 50ms 이상 UI 스레드를 차단하지 마십시오.
  • UI 스레드에서 초당 최대 100 개의 연속을 예약 할 수 있습니다. 1000이 너무 큽니다.

사용해야하는 두 가지 기술이 있습니다.

1) 가능할 ConfigureAwait(false)때 사용하십시오 .

예, await MyAsync().ConfigureAwait(false);대신 await MyAsync();.

ConfigureAwait(false)await현재 컨텍스트에서 다시 시작할 필요가 없음을 알려줍니다 (이 경우 "현재 컨텍스트에서"는 "UI 스레드에서"를 의미 함). 그러나 해당 async메소드 의 나머지 부분 () 뒤에는 ConfigureAwait현재 컨텍스트에 있다고 가정하는 작업 (예 : 업데이트 UI 요소)을 수행 할 수 없습니다.

자세한 내용은 MSDN 기사 비동기 프로그래밍의 모범 사례를 참조하십시오 .

2) Task.RunCPU 바운드 메소드를 호출하는 데 사용 합니다.

Task.Run재사용 할 수있는 코드 (예 : 라이브러리 코드) 내에 는 사용 하지 않아야합니다 . 따라서 메소드 구현 의 일부가 아닌 메소드 Task.Run호출 하는 데 사용 합니다 .

따라서 순수한 CPU 바운드 작업은 다음과 같습니다.

// Documentation: This method is CPU-bound.
void DoWork();

다음을 사용하여 전화하십시오 Task.Run.

await Task.Run(() => DoWork());

CPU 바운드와 I / O 바운드 가 혼합 된 메소드 에는 CPU 바운드 Async특성을 나타내는 문서 가 포함 된 서명 이 있어야합니다 .

// Documentation: This method is CPU-bound.
Task DoWorkAsync();

Task.Run(부분적으로 CPU 바인딩되어 있기 때문에)를 사용하여 호출 할 수도 있습니다 .

await Task.Run(() => DoWorkAsync());

4
빠른 답변 감사합니다! 귀하가 게시 한 링크를 알고 귀하의 블로그에서 참조되는 비디오를 보았습니다. 실제로 이것이 내가이 질문을 게시 한 이유입니다. 비디오에서 (응답과 동일) 핵심 코드에서 Task.Run을 사용해서는 안됩니다. 그러나 내 문제는 응답을 느리게하지 않기 위해 사용할 때마다 이러한 방법을 래핑해야한다는 것입니다 (모든 코드가 비동기 적이며 차단되지 않지만 Thread.Run이 없으면 지연됩니다). CPU 바인딩 된 메소드 (많은 Task.Run 호출)를 래핑하는 것이 더 나은 방법인지 또는 하나의 Task.Run에서 모든 것을 완전히 래핑하는 것이 더 혼란 스럽습니까?
Lukas K

12
모든 라이브러리 메소드는를 사용해야합니다 ConfigureAwait(false). 먼저 그렇게하면 Task.Run완전히 불필요 할 수 있습니다 . 당신이 경우 않는 여전히 필요 Task.Run, 그것은 당신이 한 번 또는 여러 번 호출 여부를이 경우 런타임에 많은 차이가되지 않습니다 그래서 그냥 코드에 대한 가장 자연스러운 일을.
Stephen Cleary

2
첫 번째 기술이 어떻게 도움이 될지 모르겠습니다. ConfigureAwait(false)cpu-bound 메소드에서 사용하더라도 cpu-bound 메소드를 수행하는 것은 여전히 ​​UI 스레드이며 TP 스레드에서 수행되는 모든 작업 만 수행 할 수 있습니다. 아니면 내가 잘못 이해 했습니까?
다리우스

4
@ user4205580 : 아니요. Task.Run비동기 서명을 이해하므로 완료 될 때까지 DoWorkAsync완료 되지 않습니다 . 추가 async/ await불필요합니다. 에티켓 에 대한 블로그 시리즈Task.Run 에서 "이유"에 대해 더 설명 합니다.
Stephen Cleary

3
@ user4205580 : 아니요. 대다수의 "코어"비동기 메소드는 내부적으로 사용하지 않습니다. 정상 은 "핵심"비동기 방법을 구현하는 방법은 사용하는 것 TaskCompletionSource<T>또는 등의 속기 표기법 중 하나 FromAsync. 비동기 메소드에 스레드가 필요없는 이유를 자세히 설명 하는 블로그 게시물이 있습니다.
Stephen Cleary

12

ContentLoader의 한 가지 문제는 내부적으로 순차적으로 작동한다는 것입니다. 더 나은 패턴은 작업을 병렬화 한 다음 마지막에 동기화하는 것입니다.

public class PageViewModel : IHandle<SomeMessage>
{
   ...

   public async void Handle(SomeMessage message)
   {
      ShowLoadingAnimation();

      // makes UI very laggy, but still not dead
      await this.contentLoader.LoadContentAsync(); 

      HideLoadingAnimation();   
   }
}

public class ContentLoader 
{
    public async Task LoadContentAsync()
    {
        var tasks = new List<Task>();
        tasks.Add(DoCpuBoundWorkAsync());
        tasks.Add(DoIoBoundWorkAsync());
        tasks.Add(DoCpuBoundWorkAsync());
        tasks.Add(DoSomeOtherWorkAsync());

        await Task.WhenAll(tasks).ConfigureAwait(false);
    }
}

당연히 다른 작업의 데이터가 필요한 작업이있는 경우에는 작동하지 않지만 대부분의 시나리오에서 더 나은 전체 처리량을 제공해야합니다.

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