[c#] .NET에서 제어량 흐름을 어떻게 산출하고 기다려야합니까?


1 Answers

yield 은이 둘 중 더 쉬우므로이를 살펴 보겠습니다.

우리가 가진 말 :

public IEnumerable<int> CountToTen()
{
  for (int i = 1; i <= 10; ++i)
  {
    yield return i;
  }
}

이것은 우리가 작성한 것처럼 약간 컴파일됩니다.

// Deliberately use name that isn't valid C# to not clash with anything
private class <CountToTen> : IEnumerator<int>, IEnumerable<int>
{
    private int _i;
    private int _current;
    private int _state;
    private int _initialThreadId = CurrentManagedThreadId;

    public IEnumerator<CountToTen> GetEnumerator()
    {
        // Use self if never ran and same thread (so safe)
        // otherwise create a new object.
        if (_state != 0 || _initialThreadId != CurrentManagedThreadId)
        {
            return new <CountToTen>();
        }

        _state = 1;
        return this;
    }

    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();

    public int Current => _current;

    object IEnumerator.Current => Current;

    public bool MoveNext()
    {
        switch(_state)
        {
            case 1:
                _i = 1;
                _current = i;
                _state = 2;
                return true;
            case 2:
                ++_i;
                if (_i <= 10)
                {
                    _current = _i;
                    return true;
                }
                break;
        }
        _state = -1;
        return false;
    }

    public void Dispose()
    {
      // if the yield-using method had a `using` it would
      // be translated into something happening here.
    }

    public void Reset()
    {
        throw new NotSupportedException();
    }
}

따라서 IEnumerable<int>IEnumerator<int> 의 손으로 작성된 구현 (예 :이 경우 별도의 _state , _i_current 가 필요하지 않음)이 아니라 효율적이지는 않습니다 (재사용의 트릭 새로운 객체를 만드는 것보다 안전 할 때 자체적으로 수행하는 것이 좋다), 매우 복잡한 yield 을 처리하기 위해 확장 할 수있다.

그리고 물론

foreach(var a in b)
{
  DoSomething(a);
}

와 같다:

using(var en = b.GetEnumerator())
{
  while(en.MoveNext())
  {
     var a = en.Current;
     DoSomething(a);
  }
}

그런 다음 생성 된 MoveNext() 반복적으로 호출됩니다.

async 경우는 거의 같은 원리이지만 약간의 복잡성이 있습니다. 다른 응답 코드에서 예제를 다시 사용하려면 다음과 같이하십시오.

private async Task LoopAsync()
{
    int count = 0;
    while(count < 5)
    {
       await SomeNetworkCallAsync();
       count++;
    }
}

다음과 같은 코드를 생성합니다.

private struct LoopAsyncStateMachine : IAsyncStateMachine
{
  public int _state;
  public AsyncTaskMethodBuilder _builder;
  public TestAsync _this;
  public int _count;
  private TaskAwaiter _awaiter;
  void IAsyncStateMachine.MoveNext()
  {
    try
    {
      if (_state != 0)
      {
        _count = 0;
        goto afterSetup;
      }
      TaskAwaiter awaiter = _awaiter;
      _awaiter = default(TaskAwaiter);
      _state = -1;
    loopBack:
      awaiter.GetResult();
      awaiter = default(TaskAwaiter);
      _count++;
    afterSetup:
      if (_count < 5)
      {
        awaiter = _this.SomeNetworkCallAsync().GetAwaiter();
        if (!awaiter.IsCompleted)
        {
          _state = 0;
          _awaiter = awaiter;
          _builder.AwaitUnsafeOnCompleted<TaskAwaiter, TestAsync.LoopAsyncStateMachine>(ref awaiter, ref this);
          return;
        }
        goto loopBack;
      }
      _state = -2;
      _builder.SetResult();
    }
    catch (Exception exception)
    {
      _state = -2;
      _builder.SetException(exception);
      return;
    }
  }
  [DebuggerHidden]
  void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine param0)
  {
    _builder.SetStateMachine(param0);
  }
}

public Task LoopAsync()
{
  LoopAsyncStateMachine stateMachine = new LoopAsyncStateMachine();
  stateMachine._this = this;
  AsyncTaskMethodBuilder builder = AsyncTaskMethodBuilder.Create();
  stateMachine._builder = builder;
  stateMachine._state = -1;
  builder.Start(ref stateMachine);
  return builder.Task;
}

그것은 더 복잡하지만 매우 유사한 기본 원리입니다. 주된 추가 복잡성은 현재 GetAwaiter() 가 사용되고 있다는 것입니다. awaiter.IsCompleted 가 체크되면 그것이 await 있는 작업이 이미 완료 되었기 때문에 (예를 들어 동 기적으로 리턴 할 수있는 경우) 메소드가 상태를 통해 계속 이동하지만 그렇지 않은 경우 대기 상태의 콜백으로 설정됩니다.

그게 무슨 일이 일어나는가는 awaiter에 달려 있습니다. 무엇이 콜백을 트리거하는지 (비동기 I / O 완료, 쓰레드 완료시 실행되는 태스크), 특정 쓰레드로 정렬하거나 쓰레드 풀 쓰레드에서 실행하기위한 요구 사항이 무엇인지에 달려 있습니다 원래 통화의 컨텍스트가 필요할 수도 있고 그렇지 않을 수도 있습니다. 대기자의 무언가가 MoveNext 를 호출 할지라도 다음 작업 (다음 await ) 또는 완료 및 반환 중 어떤 경우에 구현중인 Task 이 완료 될 것입니다.

Question

yield 키워드를 이해하면 반복자 블록 내부에서 사용하면 호출 코드에 제어 흐름이 반환되고 반복기가 다시 호출되면 중단 된 부분이 다시 선택됩니다.

또한 피 호출자를 await 뿐만 아니라 호출자에게 제어권을 반환합니다. 호출자 awaits 메서드를 awaits 때 중단 한 부분을 픽업합니다.

다른 말로하면 스레드가없고 비동기와 대기의 "동시성"은 제어의 영리한 흐름으로 인한 환상이며 그 세부 사항은 구문에 의해 숨겨져 있습니다.

이제는 전 어셈블리 프로그래머이고 명령 포인터, 스택 등을 잘 알고 있고 정상적인 제어 흐름 (서브 루틴, 재귀, 루프, 분기)이 어떻게 작동 하는지를 알 수 있습니다. 그러나이 새로운 구조들 - 나는 그것을 얻지 못합니다.

await 때 런타임에서 어떤 코드가 다음에 실행되어야하는지 어떻게 알 수 있습니까? 중단 된 곳에서 언제 다시 시작할 수 있는지 어떻게 알 수 있습니까? 어떻게 기억합니까? 현재 호출 스택은 어떻게됩니까? 어떻게 든 저장됩니까? 호출하는 메소드가 다른 메소드 호출을 await 나서 스택을 겹쳐 쓰지 않는 이유는 무엇입니까? 그리고 예외가 발생하고 스택이 풀린다면 런타임은이 모든 것을 통해 어떻게 작동할까요?

yield 에 도달하면 런타임에서 물건을 가져와야하는 지점을 어떻게 추적합니까? 반복자 상태는 어떻게 보존됩니까?




yieldawait 동안 흐름 제어를 다루는 동안, 완전히 다른 두 가지. 그래서 나는 그것들을 따로 따로 다룰 것이다.

yield 의 목표는 게으른 시퀀스를 쉽게 빌드 할 수있게하는 것입니다. yield 문을 포함하는 열거 자 루프를 작성하면 컴파일러는 표시되지 않는 많은 새 코드를 생성합니다. 후드에서 실제로 완전히 새로운 클래스를 생성합니다. 이 클래스에는 루프 상태를 추적하는 멤버와 IEnumerable 구현이 포함되어 있으므로 MoveNext 를 호출 할 때마다 루프를 통해 한 번 더 단계를 밟습니다. 그래서 foreach 루프를 이렇게하면 :

foreach(var item in mything.items()) {
    dosomething(item);
}

생성 된 코드는 다음과 같습니다.

var i = mything.items();
while(i.MoveNext()) {
    dosomething(i.Current);
}

mything.items ()의 구현 내부는 루프의 "단계"를 수행 한 다음 리턴하는 상태 머신 코드입니다. 따라서 간단한 루프처럼 소스에 쓰는 동안 간단한 루프가 아닙니다. 그래서 컴파일러는 속임수입니다. 자신을보고 싶다면 ILDASM 또는 ILSpy 또는 유사한 도구를 추출하고 생성 된 IL이 어떻게 보이는지 확인하십시오. 그것은 유익해야합니다.

asyncawait 은 다른 한편으로 물고기의 다른 주전자입니다. Await은 추상적 인면에서 동기화 기본 요소입니다. 이것은 시스템에 "이 작업이 완료 될 때까지 계속할 수 없다"라고 말하는 방법입니다. 그러나, 당신이 언급했듯이, 항상 스레드가 관련된 것은 아닙니다.

관련된 내용 동기화 컨텍스트라고하는 것입니다. 항상 주위에 매달려있는 사람이 있습니다. 동기화 컨텍스트의 작업은 기다리고있는 작업과 계속 작업을 예약하는 것입니다.

당신 await thisThing() 있다고 말할 때, 몇 가지 일이 일어납니다. 비동기 메서드에서는 컴파일러가 메서드를 실제로 작은 단위로 잘라냅니다. 각 청크는 "before await"섹션과 "await"(또는 continuation) 섹션 뒤에 있습니다. 대기가 실행되면 작업이 대기되고 다음 연속, 즉 나머지 함수가 동기화 컨텍스트로 전달됩니다. 컨텍스트는 작업 스케줄링을 처리하고, 작업이 끝나면 컨텍스트는 연속성을 실행하고 원하는 반환 값을 전달합니다.

동기화 컨텍스트는 일정을 정하는 한 원하는 모든 작업을 수행 할 수 있습니다. 스레드 풀을 사용할 수 있습니다. 작업 당 스레드를 만들 수 있습니다. 그것들을 동 기적으로 실행할 수 있습니다. 서로 다른 환경 (ASP.NET과 WPF)은 환경에 가장 적합한 것을 기반으로 서로 다른 작업을 수행하는 다양한 동기화 컨텍스트 구현을 제공합니다.

(보너스 : .ConfigurateAwait(false) 궁금합니다.) 현재 동기화 컨텍스트 (예 : WPF 대 ASP.NET)를 사용하지 말고 대신 기본 동기화 컨텍스트를 사용합니다. 스레드 풀).

다시 한 번, 컴파일러는 많이 속임수입니다. 생성 된 코드를 보면 복잡하지만 실제로 무엇을하는지 볼 수 있어야합니다. 이러한 종류의 변환은 어렵지만 결정적이며 수학적입니다. 따라서 컴파일러가 이러한 변환을 수행하는 것이 좋습니다.

추신 : 기본 동기화 컨텍스트의 존재에 대한 한 가지 예외가 있습니다. 콘솔 앱에는 기본 동기화 컨텍스트가 없습니다. 자세한 정보는 Stephen Toub의 블로그 를 확인하십시오. async 대한 정보를 찾고 일반적으로 await 좋은 장소입니다.




Related