스레드가 while 루프 내에서 작업을 기다리는 경우 정확히 어떻게됩니까?


10

C #의 async / await 패턴을 잠시 동안 처리 한 후 갑자기 다음 코드에서 발생하는 상황을 설명하는 방법을 모른다는 것을 깨달았습니다.

async void MyThread()
{
    while (!_quit)
    {
        await GetWorkAsync();
    }
}

GetWorkAsync()Task연속이 실행될 때 스레드 전환을 야기 할 수도 있고 그렇지 않을 수도 있는 대기를 리턴하는 것으로 가정된다 .

기다림이 루프 내부에 없다면 혼란스럽지 않을 것입니다. 필자는 자연스럽게 나머지 메소드 (예 : 연속)가 다른 스레드에서 실행될 가능성이 높습니다.

그러나 루프 내부에서 "나머지 방법"이라는 개념은 약간 안개가납니다.

스레드가 계속 켜져 있거나 켜져 있지 않은 경우 "나머지 루프"는 어떻게됩니까? 루프의 다음 반복이 어느 스레드에서 실행됩니까?

내 관찰에 따르면 각 반복이 동일한 스레드 (원래 스레드)에서 시작되는 동안 계속이 다른 스레드에서 실행되는 것으로 나타났습니다. 이것이 정말로 가능할까요? 그렇다면, 이것이 GetWorkAsync 메소드의 스레드 안전성에 대해 설명해야하는 예기치 않은 병렬 처리 수준입니까?

업데이트 : 내 질문은 일부에서 제안한 것처럼 중복되지 않습니다. while (!_quit) { ... }코드 패턴은 단지 내 실제 코드의 단순화이다. 실제로 내 스레드는 일정 간격 (기본적으로 5 초마다)으로 작업 항목의 입력 큐를 처리하는 수명이 긴 루프입니다. 실제 종료 조건 확인은 샘플 코드에서 제안한 간단한 필드 확인이 아니라 이벤트 핸들 확인입니다.



1
.NET에서 yield and await는 제어 흐름을 구현하는 방법을 참조하십시오 . 이것이 어떻게 연결되어 있는지에 대한 훌륭한 정보가 필요합니다.
John Wu

@ 존 우 : 아직 그 스레드를 보지 못했습니다. 흥미로운 정보가 많이 있습니다. 감사!
aoven

답변:


6

실제로 Roslyn 시도 에서 확인할 수 있습니다 . await 메소드는 void IAsyncStateMachine.MoveNext()생성 된 비동기 클래스에서 다시 작성됩니다 .

당신이 볼 것은 다음과 같습니다.

            if (this.state != 0)
                goto label_2;
            //set up the state machine here
            label_1:
            taskAwaiter.GetResult();
            taskAwaiter = default(TaskAwaiter);
            label_2:
            if (!OuterClass._quit)
            {
               taskAwaiter = GetWorkAsync().GetAwaiter();
               //state machine stuff here
            }
            goto label_1;

기본적으로 어떤 스레드에 있는지는 중요하지 않습니다. 루프를 동등한 if / goto 구조로 교체하여 상태 머신이 올바르게 재개 될 수 있습니다.

그러나 비동기 메소드가 반드시 다른 스레드에서 실행되는 것은 아닙니다. 한 스레드에서만 작업 할 수있는 방법을 설명하려면 Eric Lippert의 설명 "매직이 아닙니다"를 참조하십시오 async/await.


2
컴파일러가 비동기 코드에서 수행하는 재 작성 범위를 과소 평가하는 것 같습니다. 본질적으로, 재 작성 후에는 "루프"가 없습니다! 그것은 저에게 없어진 부분이었습니다. 'Roslyn 시도'링크도 굉장합니다.
aoven

GOTO는 원래 루프 구조입니다. 잊지 말자.

2

첫째, Servy는 비슷한 질문에 대한 답변으로 몇 가지 코드를 작성했습니다.

/programming/22049339/how-to-create-a-cancellable-task-loop

Servy의 답변에는 and 및 키워드를 ContinueWith()명시 적으로 사용하지 않고 TPL 구문을 사용 하는 유사한 루프가 포함됩니다 . 질문에 대답하기 위해 루프를 풀 때 코드가 어떻게 보일지 고려하십시오.asyncawaitContinueWith()

    private static Task GetWorkWhileNotQuit()
    {
        var tcs = new TaskCompletionSource<bool>();

        Task previous = Task.FromResult(_quit);
        Action<Task> continuation = null;
        continuation = t =>
        {
            if (!_quit)
            {
                previous = previous.ContinueWith(_ => GetWorkAsync())
                    .Unwrap()
                    .ContinueWith(_ => previous.ContinueWith(continuation));
            }
            else
            {
                tcs.SetResult(_quit);
            }
        };
        previous.ContinueWith(continuation);
        return tcs.Task;
    }

머리를 감싸는 데 시간이 걸리지 만 요약하면 다음과 같습니다.

  • continuation"현재 반복"에 대한 폐쇄를 나타냅니다.
  • previous"이전 반복"Task 의 상태를 포함 함을 나타냅니다 (즉, '반복'이 완료된시기를 알고 다음 반복을 시작하는 데 사용됨).
  • GetWorkAsync()반환 한다고 가정하면 '내부 작업'(즉, 실제 결과 ) 을 얻기위한 호출을 반환 Task한다는 의미 ContinueWith(_ => GetWorkAsync())입니다 .Task<Task>Unwrap()GetWorkAsync()

그래서:

  1. 처음에는 이전 반복 이 없으므로 단순히 값이 할당 Task.FromResult(_quit) 됩니다 Task.Completed == true. 상태는로 시작됩니다 .
  2. continuation사용하여 처음으로 실행previous.ContinueWith(continuation)
  3. 완료 상태를 반영 하여 continuation클로저 업데이트previous_ => GetWorkAsync()
  4. _ => GetWorkAsync()완료, 그것은 "계속" _previous.ContinueWith(continuation)- 즉, 호출 continuation을 다시 람다
    • 분명히이 시점 previous에서 상태로 업데이트 _ => GetWorkAsync()되었으므로 continuation람다는 GetWorkAsync()반환 할 때 호출됩니다 .

continuation람다는 항상의 상태를 확인 _quit하는 경우, 정도 _quit == false후 더 이상 연속성을가없고,이 TaskCompletionSource값으로 설정됩니다 _quit, 모든 것이 완료됩니다.

다른 스레드에서 실행되는 연속성에 대한 귀하의 관찰에 관해서는 이 블로그에서 "태스크는 여전히 스레드가 아니며 비동기는 병렬이 아닙니다"와 같이 async/ await키워드가 당신을 위해 할 일 이 아닙니다 . -https : //blogs.msdn.microsoft.com/benwilli/2015/09/10/tasks-are-still-not-threads-and-async-is-not-parallel/

GetWorkAsync()스레딩 및 스레드 안전성과 관련하여 귀하의 방법을 자세히 살펴볼 가치가 있다고 제안합니다 . 진단에서 반복 된 비동기 / 대기 코드의 결과로 다른 스레드에서 실행 중임을 표시하는 경우 해당 메소드 내부 또는 관련 메소드로 인해 다른 스레드에서 새 스레드가 작성되어야합니다. (이것이 예기치 않은 경우 .ConfigureAwait어딘가에 있습니까?)


2
내가 보여준 코드는 (매우) 단순화되었습니다. GetWorkAsync () 내부에는 여러 개의 추가 대기가 있습니다. 그들 중 일부는 데이터베이스와 네트워크에 액세스하므로 진정한 I / O를 의미합니다. 내가 이해하는 것처럼, 스레드 스위치는 초기 스레드가 연속이 실행되는 위치를 제어하는 ​​동기화 컨텍스트를 설정하지 않기 때문에 그러한 대기의 자연스러운 결과입니다 (필요하지는 않지만). 따라서 스레드 풀 스레드에서 실행됩니다. 내 추론이 잘못 되었습니까?
aoven

@aoven 좋은 점-나는 다른 종류를 고려하지 않았다-SynchronizationContext를 사용하여 연속을 디스패치 한 SynchronizationContext이후 확실히 중요하다 .ContinueWith(). 실제로 awaitThreadPool 스레드 또는 ASP.NET 스레드에서 호출 되는지 확인하는 동작을 설명합니다 . 이러한 경우 연속이 다른 스레드로 전달 될 수 있습니다. 반면, awaitWPF 디스패처 또는 Winforms 컨텍스트와 같은 단일 스레드 컨텍스트를 호출 하면 원본에서 연속성이 보장되도록 충분해야합니다. 스레드
벤 Cottrell
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.