Task.WhenAll의 연속이 동 기적으로 실행되는 이유는 무엇입니까?


14

Task.WhenAll.NET Core 3.0에서 실행할 때 방법 에 대해 궁금한 점을 확인했습니다 . 간단한 Task.Delay작업을에 단일 인수로 전달 Task.WhenAll했으며 래핑 된 작업이 원래 작업과 동일하게 작동 할 것으로 예상했습니다. 그러나 이것은 사실이 아닙니다. 원래 작업의 연속은 비동기식으로 (바람직하게) Task.WhenAll(task)실행되고 여러 래퍼 의 연속은 연속적으로 동 기적으로 실행됩니다 (바람직하지 않음).

다음은 이 동작 의 데모 입니다. 4 명의 작업자 작업이 동일한 Task.Delay작업을 완료하기를 기다리고있다 가 무거운 계산을 계속합니다 (로 시뮬레이션 됨 Thread.Sleep).

var task = Task.Delay(500);
var workers = Enumerable.Range(1, 4).Select(async x =>
{
    Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff}" +
        $" [{Thread.CurrentThread.ManagedThreadId}] Worker{x} before await");

    await task;
    //await Task.WhenAll(task);

    Console.WriteLine($"{DateTime.Now:HH:mm:ss.fff}" +
        $" [{Thread.CurrentThread.ManagedThreadId}] Worker{x} after await");

    Thread.Sleep(1000); // Simulate some heavy CPU-bound computation
}).ToArray();
Task.WaitAll(workers);

출력은 다음과 같습니다. 4 개의 연속이 다른 스레드에서 예상대로 실행됩니다 (병렬).

05:23:25.511 [1] Worker1 before await
05:23:25.542 [1] Worker2 before await
05:23:25.543 [1] Worker3 before await
05:23:25.543 [1] Worker4 before await
05:23:25.610 [4] Worker1 after await
05:23:25.610 [7] Worker2 after await
05:23:25.610 [6] Worker3 after await
05:23:25.610 [5] Worker4 after await

이제 줄 await task을 주석 처리하고 다음 줄의 주석 처리를 제거 await Task.WhenAll(task)하면 출력이 상당히 다릅니다. 모든 연속체가 동일한 스레드에서 실행되므로 계산이 병렬화되지 않습니다. 각 계산은 이전 계산이 완료된 후 시작됩니다.

05:23:46.550 [1] Worker1 before await
05:23:46.575 [1] Worker2 before await
05:23:46.576 [1] Worker3 before await
05:23:46.576 [1] Worker4 before await
05:23:46.645 [4] Worker1 after await
05:23:47.648 [4] Worker2 after await
05:23:48.650 [4] Worker3 after await
05:23:49.651 [4] Worker4 after await

놀랍게도 이것은 각 작업자가 다른 랩퍼를 기다리는 경우에만 발생합니다. 래퍼를 미리 정의하면 :

var task = Task.WhenAll(Task.Delay(500));

... 그리고 await모든 작업자 내에서 동일한 작업을 수행하면 동작은 첫 번째 경우와 동일합니다 (비동기 연속).

내 질문은 : 왜 이런 일이 발생합니까? 동일한 작업의 다른 래퍼의 연속이 동일한 스레드에서 동기식으로 실행되는 원인은 무엇입니까?

참고 :Task.WhenAny 대신 작업을 래핑 Task.WhenAll하면 동일한 이상한 동작이 발생합니다.

또 다른 관찰 : 래퍼를 a 안에 래핑 Task.Run하면 연속성이 비 동기화 될 것으로 예상했습니다 . 그러나 일어나지 않습니다. 아래 줄의 연속은 여전히 ​​동일한 스레드에서 (동기식으로) 실행됩니다.

await Task.Run(async () => await Task.WhenAll(task));

설명 : 위의 차이점은 .NET Core 3.0 플랫폼에서 실행되는 콘솔 응용 프로그램에서 관찰되었습니다. .NET Framework 4.8에서는 원래 작업을 기다리는 것과 작업 래퍼를 ​​기다리는 것 사이에는 차이가 없습니다. 두 경우 모두 연속이 동일한 스레드에서 동기식으로 실행됩니다.


궁금한 점이 있다면 await Task.WhenAll(new[] { task });어떻게됩니까?
vasily.sib

1
내부 단락으로 인한 것 같아요Task.WhenAll
Michael Randall

3
LinqPad는 두 변형 모두에 대해 동일한 예상되는 두 번째 출력을 제공합니다 ... 병렬 실행을 얻는 데 사용하는 환경 (콘솔 vs. WinForms vs ..., .NET vs. 코어, ..., 프레임 워크 버전)?
Alexei Levenkov

1
.NET Core 3.0 및 3.1에서이 동작을 복제 할 수 있었지만 ed에서 완료되지 않도록 초기를 Task.Delay에서 100로 변경 한 후에 만 ​​가능 합니다. 1000await
Stephen Cleary

2
@BlueStrat 좋은 발견! 그것은 어떻게 든 관련이있을 수 있습니다. 흥미롭게도 .NET Frameworks 4.6, 4.6.1, 4.7.1, 4.7.2 및 4.8 에서 Microsoft 코드 의 잘못된 동작을 재현하지 못했습니다 . 매번 다른 스레드 ID를 얻습니다. 이는 올바른 동작입니다. 다음 은 4.7.2에서 실행되는 바이올린입니다.
Theodor Zoulias

답변:


2

따라서 동일한 작업 변수를 기다리는 여러 비동기 메소드가 있습니다.

    await task;
    // CPU heavy operation

예, 이러한 연속은 task완료되면 연속적으로 호출 됩니다. 귀하의 예에서, 각 연속은 다음 초 동안 스레드를 호그합니다.

각 연속을 비동기 적으로 실행하려면 다음과 같은 것이 필요할 수 있습니다.

    await task;
    await Task.Yield().ConfigureAwait(false);
    // CPU heavy operation

따라서 작업이 초기 연속에서 복귀하고 CPU로드가 외부에서 실행되도록합니다 SynchronizationContext.


답변 해 주셔서 감사합니다. 예, Task.Yield내 문제에 대한 좋은 해결책입니다. 그러나 내 질문은 왜 이런 일이 일어나고 있는지, 원하는 행동을 강요하는 방법에 대해서는 적습니다.
Theodor Zoulias

정말로 알고 싶다면 소스 코드가 여기 있습니다. github.com/microsoft/referencesource/blob/master/mscorlib/…
Jeremy Lakeman

관련 클래스의 소스 코드를 연구하여 내 질문에 대한 답변을 얻는 것이 그렇게 간단하기를 바랍니다. 코드를 이해하고 무슨 일이 일어나고 있는지 알아내는 데 오랜 시간이 걸릴 것입니다!
Theodor Zoulias

열쇠는를 피하고, 원래 작업에서 한 번만 SynchronizationContext호출 ConfigureAwait(false)하면 충분합니다.
Jeremy Lakeman

이것은 콘솔 응용 프로그램 SynchronizationContext.Current이며 null입니다. 하지만 방금 확인했습니다. 나는 라인에 추가 ConfigureAwait(false)했는데 await아무런 차이가 없었습니다. 관측치는 이전과 동일합니다.
Theodor Zoulias

1

을 사용하여 작업을 만들면 작업 Task.Delay()작성 옵션이 None아닌 으로 설정됩니다 RunContinuationsAsychronously.

.net 프레임 워크와 .net 코어 사이의 변경을 방해 할 수 있습니다. 그럼에도 불구하고 관찰하고있는 행동을 설명하는 것으로 보입니다. 또한 소스 코드에 파고에서이를 확인할 수 Task.Delay()있다 최대 newingDelayPromise 기본 호출하는 Task지정되지 생성 옵션을 남기지 않고 생성자를.


답변을 주셔서 감사합니다 Tanveer. 따라서 .NET Core에서 새 객체를 생성 할 때 RunContinuationsAsychronously기본값이 대신되었다고 추측하십시오 . 이것은 내 관찰 중 일부를 설명하지만 전부는 아닙니다. 특히 동일한 래퍼를 기다리는 것과 다른 래퍼를 기다리는 것의 차이점을 설명하지 않습니다 . NoneTaskTask.WhenAll
Theodor Zoulias

0

귀하의 코드에서 다음 코드는 반복 본문이 아닙니다.

var task = Task.Delay(100);

따라서 다음을 실행할 때마다 작업을 기다리고 별도의 스레드에서 실행합니다.

await task;

그러나 다음을 실행하면의 상태를 확인 task하므로 하나의 스레드에서 실행됩니다.

await Task.WhenAll(task);

그러나 작업 생성을 옆으로 옮기면 WhenAll각 작업이 별도의 스레드에서 실행됩니다.

var task = Task.Delay(100);
await Task.WhenAll(task);

답변 주셔서 감사합니다 Seyedraouf. 당신의 설명은 나에게 너무 만족스럽지 않습니다. 에 의해 반환 된 작업 은 원본과 마찬가지로 Task.WhenAll규칙적 입니다. 두 작업 모두 어느 시점에서 완료되며, 타이머 이벤트의 결과로 원본과 원본 작업이 완료된 결과로 합성됩니다. 왜 연속이 다른 동작을 보여야합니까? 한 작업이 다른 작업과 다른 점은 무엇입니까? Tasktask
Theodor Zoulias
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.