[C#] await와 ContinueWith의 차이점

다음 예에서 누군가 awaitContinueWith동의어인지 아닌지 설명 할 수 있습니까 ? 나는 처음으로 TPL을 사용하려고 노력하고 있으며 모든 문서를 읽었지만 차이점을 이해하지 못합니다.

기다립니다 :

String webText = await getWebPage(uri);
await parseData(webText);

ContinueWith :

Task<String> webText = new Task<String>(() => getWebPage(uri));
Task continue = webText.ContinueWith((task) =>  parseData(task.Result));
webText.Start();
continue.Wait();

특정 상황에서 하나가 다른 것보다 선호됩니까?



답변

두 번째 코드에서는 연속 작업이 완료 될 때까지 동 기적으로 대기합니다. 첫 번째 버전에서 메서드는 await아직 완료되지 않은 첫 번째 표현식에 도달 하자마자 호출자에게 반환됩니다 .

둘 다 연속을 예약한다는 점에서 매우 유사하지만 제어 흐름이 약간 복잡해지면 코드 await훨씬 더 간단 해 집니다 . 또한 Servy가 주석에서 언급했듯이 작업을 기다리는 것은 일반적으로 더 간단한 오류 처리로 이어지는 집계 예외를 “언 래핑”합니다. 또한를 사용 await하면 호출 컨텍스트에서 연속을 암시 적으로 예약합니다 (를 사용하지 않는 경우 ConfigureAwait). “수동으로”수행 할 수없는 것은 아니지만 .NET으로 수행하는 것이 훨씬 쉽습니다 await.

난 당신이 모두 작업의 약간 큰 순서를 구현하려고 제안 await하고 Task.ContinueWith– 그것은 진짜 눈을 뜨게 할 수있다.


답변

다음은 비동기 해결을 사용하여 차이점과 다양한 문제를 설명하기 위해 최근에 사용한 코드 조각의 시퀀스입니다.

GUI 기반 애플리케이션에 많은 시간이 걸리는 이벤트 핸들러가있어서이를 비동기로 만들고 싶다고 가정 해 보겠습니다. 시작하는 동기 논리는 다음과 같습니다.

while (true) {
    string result = LoadNextItem().Result;
    if (result.Contains("target")) {
        Counter.Value = result.Length;
        break;
    }
}

LoadNextItem은 작업을 반환하며 결국 검사하려는 결과를 생성합니다. 현재 결과가 찾고있는 결과이면 UI에서 일부 카운터 값을 업데이트하고 메서드에서 반환합니다. 그렇지 않으면 LoadNextItem에서 더 많은 항목을 계속 처리합니다.

비동기 버전에 대한 첫 번째 아이디어 : 계속 사용하십시오! 그리고 당분간 루핑 부분을 무시합시다. 내 말은, 무엇이 잘못 될 수 있습니까?

return LoadNextItem().ContinueWith(t => {
    string result = t.Result;
    if (result.Contains("target")) {
        Counter.Value = result.Length;
    }
});

좋습니다. 이제 차단하지 않는 메서드가 있습니다! 대신 충돌합니다. UI 컨트롤에 대한 모든 업데이트는 UI 스레드에서 발생해야하므로이를 고려해야합니다. 고맙게도 연속 작업을 예약하는 방법을 지정하는 옵션이 있으며 이에 대한 기본 옵션이 있습니다.

return LoadNextItem().ContinueWith(t => {
    string result = t.Result;
    if (result.Contains("target")) {
        Counter.Value = result.Length;
    }
},
TaskScheduler.FromCurrentSynchronizationContext());

좋습니다. 이제 충돌하지 않는 방법이 있습니다! 대신 조용히 실패합니다. 연속 작업은 이전 작업의 상태와 관련이없는 별도의 작업 자체입니다. 따라서 LoadNextItem에 오류가 있더라도 호출자는 성공적으로 완료된 작업 만 볼 수 있습니다. 좋아요, 예외가 있다면 그냥 넘겨주세요 :

return LoadNextItem().ContinueWith(t => {
    if (t.Exception != null) {
        throw t.Exception.InnerException;
    }
    string result = t.Result;
    if (result.Contains("target")) {
        Counter.Value = result.Length;
    }
},
TaskScheduler.FromCurrentSynchronizationContext());

이제 실제로 작동합니다. 단일 항목의 경우. 이제 루핑은 어떻습니까? 결과적으로 원래 동기 버전의 논리와 동일한 솔루션은 다음과 같습니다.

Task AsyncLoop() {
    return AsyncLoopTask().ContinueWith(t =>
        Counter.Value = t.Result,
        TaskScheduler.FromCurrentSynchronizationContext());
}
Task<int> AsyncLoopTask() {
    var tcs = new TaskCompletionSource<int>();
    DoIteration(tcs);
    return tcs.Task;
}
void DoIteration(TaskCompletionSource<int> tcs) {
    LoadNextItem().ContinueWith(t => {
        if (t.Exception != null) {
            tcs.TrySetException(t.Exception.InnerException);
        } else if (t.Result.Contains("target")) {
            tcs.TrySetResult(t.Result.Length);
        } else {
            DoIteration(tcs);
        }});
}

또는 위의 모든 것 대신 async를 사용하여 동일한 작업을 수행 할 수 있습니다.

async Task AsyncLoop() {
    while (true) {
        string result = await LoadNextItem();
        if (result.Contains("target")) {
            Counter.Value = result.Length;
            break;
        }
    }
}

이제 훨씬 더 멋지지 않습니까?


답변