[C#] C # 5 비동기 CTP : EndAwait 호출 전에 생성 된 코드에서 내부“상태”가 0으로 설정된 이유는 무엇입니까?

어제는 생성 된 코드가 어떻게 생겼는지에 특히 탐구에서, 기능 “비동기”새로운 C 번호에 대한 이야기를 제공하고, 한 the GetAwaiter()/ BeginAwait()/ EndAwait()전화.

우리는 C # 컴파일러에 의해 생성 된 상태 머신에 대해 자세히 살펴 보았고 이해할 수없는 두 가지 측면이있었습니다.

  • 생성 된 클래스에 Dispose()메소드와 $__disposing변수 가 포함되어 있는데 왜 사용되지 않는 것 같습니다 (그리고 클래스는 구현하지 않습니다 IDisposable).
  • 0이 일반적으로 “이것은 초기 진입 점”을 의미하는 것처럼 보일 때 내부 state변수가를 호출하기 전에 0으로 설정된 이유 EndAwait()입니다.

나는 누군가가 더 많은 정보를 가지고 있다면 그것을 기뻐할지라도 비동기 방법 내에서 더 흥미로운 것을 수행함으로써 첫 번째 요점에 대답 할 수 있다고 생각합니다. 그러나이 질문은 두 번째 요점에 관한 것입니다.

다음은 매우 간단한 샘플 코드입니다.

using System.Threading.Tasks;

class Test
{
    static async Task<int> Sum(Task<int> t1, Task<int> t2)
    {
        return await t1 + await t2;
    }
}

MoveNext()상태 머신을 구현하는 메소드에 대해 생성되는 코드는 다음과 같습니다 . 이것은 Reflector에서 직접 복사됩니다-말할 수없는 변수 이름을 수정하지 않았습니다 :

public void MoveNext()
{
    try
    {
        this.$__doFinallyBodies = true;
        switch (this.<>1__state)
        {
            case 1:
                break;

            case 2:
                goto Label_00DA;

            case -1:
                return;

            default:
                this.<a1>t__$await2 = this.t1.GetAwaiter<int>();
                this.<>1__state = 1;
                this.$__doFinallyBodies = false;
                if (this.<a1>t__$await2.BeginAwait(this.MoveNextDelegate))
                {
                    return;
                }
                this.$__doFinallyBodies = true;
                break;
        }
        this.<>1__state = 0;
        this.<1>t__$await1 = this.<a1>t__$await2.EndAwait();
        this.<a2>t__$await4 = this.t2.GetAwaiter<int>();
        this.<>1__state = 2;
        this.$__doFinallyBodies = false;
        if (this.<a2>t__$await4.BeginAwait(this.MoveNextDelegate))
        {
            return;
        }
        this.$__doFinallyBodies = true;
    Label_00DA:
        this.<>1__state = 0;
        this.<2>t__$await3 = this.<a2>t__$await4.EndAwait();
        this.<>1__state = -1;
        this.$builder.SetResult(this.<1>t__$await1 + this.<2>t__$await3);
    }
    catch (Exception exception)
    {
        this.<>1__state = -1;
        this.$builder.SetException(exception);
    }
}

길지만이 질문의 중요한 내용은 다음과 같습니다.

// End of awaiting t1
this.<>1__state = 0;
this.<1>t__$await1 = this.<a1>t__$await2.EndAwait();

// End of awaiting t2
this.<>1__state = 0;
this.<2>t__$await3 = this.<a2>t__$await4.EndAwait();

두 경우 모두 다음에 분명히 관찰되기 전에 상태가 다시 변경됩니다. 왜 0으로 설정해야합니까? 경우 MoveNext()이 시점에서 다시 호출했다 (직접 또는 경유 Dispose) 효과적으로 그리고 만약 … 내가 말할 수있는 늘어나는만큼 전적으로 부적합 할 수있는, 다시 비동기 방식을 시작할 것이라고 MoveNext() 되지 않는다 라고, 상태의 변화는 무관하다.

이것은 컴파일러가 반복기 블록 생성 코드를 비동기식으로 재사용하여 더 명확한 설명을 할 수있는 부작용입니까?

중요한 면책

분명히 이것은 단지 CTP 컴파일러 일뿐입니다. 최종 릴리스 이전과 다음 CTP 릴리스 이전에도 상황이 완전히 바뀔 것으로 기대합니다. 이 질문은 이것이 C # 컴파일러 또는 그와 같은 결함이라고 주장하려고 시도하지 않습니다. 나는 내가 놓친 미묘한 이유가 있는지 알아 내려고 노력하고있다. 🙂



답변

좋아, 나는 마침내 진정한 대답을 얻었다. 나는 그것을 스스로 해결했지만 팀의 VB 부분에서 Lucian Wischik이 실제로 그럴만한 이유가 있음을 확인한 후에야 해결되었습니다. 그에게 많은 감사를 전합니다- 그의 블로그 를 방문하십시오 .

여기서 값 0은 특별하기 때문에 특별합니다. 하지 방금 전에 할 수있는 유효한 상태 await보통의 경우. 특히, 상태 머신이 다른 곳에서 테스트를 마칠 수있는 상태가 아닙니다. 내가 아닌 양의 값을 사용하는 것이 단지 잘 작동 믿습니다 : 그것은으로 -1이 사용되지 않습니다 논리적으로 일반적으로 “완료”를 의미로 -1, 잘못된. 현재 상태 0에 추가 의미를 부여한다고 주장 할 수는 있지만 실제로는 중요하지 않습니다. 이 질문의 요점은 왜 국가가 전혀 설정되지 않았는지 알아내는 것이 었습니다.

발견 된 예외에서 대기가 종료되면 값이 관련됩니다. 우리는 같은 await 문으로 다시 돌아올 수 있지만, “모든 대기에서 돌아 오려고한다”는 상태 가 아니어야합니다. 그렇지 않으면 모든 종류의 코드가 건너 뜁니다. 이것을 예제와 함께 보여주는 것이 가장 간단합니다. 이제 두 번째 CTP를 사용하고 있으므로 생성 된 코드는 질문의 코드와 약간 다릅니다.

비동기 방법은 다음과 같습니다.

static async Task<int> FooAsync()
{
    var t = new SimpleAwaitable();

    for (int i = 0; i < 3; i++)
    {
        try
        {
            Console.WriteLine("In Try");
            return await t;
        }
        catch (Exception)
        {
            Console.WriteLine("Trying again...");
        }
    }
    return 0;
}

개념적으로, 그 SimpleAwaitable작업은 기다릴 수 있습니다. 어쩌면 작업이거나 다른 것일 수 있습니다. 테스트 목적으로 항상 false를 반환합니다.IsCompleted 하고에 예외를 발생 GetResult시킵니다.

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

public void MoveNext()
{
    int returnValue;
    try
    {
        int num3 = state;
        if (num3 == 1)
        {
            goto Label_ContinuationPoint;
        }
        if (state == -1)
        {
            return;
        }
        t = new SimpleAwaitable();
        i = 0;
      Label_ContinuationPoint:
        while (i < 3)
        {
            // Label_ContinuationPoint: should be here
            try
            {
                num3 = state;
                if (num3 != 1)
                {
                    Console.WriteLine("In Try");
                    awaiter = t.GetAwaiter();
                    if (!awaiter.IsCompleted)
                    {
                        state = 1;
                        awaiter.OnCompleted(MoveNextDelegate);
                        return;
                    }
                }
                else
                {
                    state = 0;
                }
                int result = awaiter.GetResult();
                awaiter = null;
                returnValue = result;
                goto Label_ReturnStatement;
            }
            catch (Exception)
            {
                Console.WriteLine("Trying again...");
            }
            i++;
        }
        returnValue = 0;
    }
    catch (Exception exception)
    {
        state = -1;
        Builder.SetException(exception);
        return;
    }
  Label_ReturnStatement:
    state = -1;
    Builder.SetResult(returnValue);
}

나는 움직여야했다 Label_ContinuationPoint유효한 코드 해야했습니다. 그렇지 않으면 goto명령문 의 범위에 속하지 않지만 답변에 영향을 미치지 않습니다.

GetResult예외가 발생하면 어떻게되는지 생각해보십시오 . 우리는 catch 블록을 거쳐 증가 i하고 다시 순환합니다 ( i아직도 3보다 작다고 가정합니다 ). 우리는 우리가 전에 있었다 어떤 상태에 아직도 GetResult전화 …하지만 우리는 안에 얻을 때 try블록 우리는 해야한다 “시도에서”인쇄하고 전화를 GetAwaiter다시 … 우리는 할 수 있습니다 상태 1.없이이 아닌 경우 그 state = 0할당, 그것은 기존의 awaiter을 사용하고 건너 뜁니다Console.WriteLine 전화를.

작업하기에는 상당히 고의적 인 코드이지만 팀이 생각해야 할 종류를 보여줍니다. 나는 이것을 구현할 책임이 없다는 것이 기쁘다. 🙂


답변

1 (첫 번째 경우)로 유지되면에 대한 호출 EndAwait없이에 대한 호출을 BeginAwait받습니다. 2 (두 번째 경우)로 유지되면 다른 대기자에게도 동일한 결과가 나타납니다.

BeginAwait를 호출하면 이미 시작된 경우 (내 생각으로) false를 반환하고 원래 값을 EndAwait에서 반환하도록 유지합니다. 이 경우 제대로 작동하지만 -1로 설정 this.<1>t__$await1하면 첫 번째 경우에 초기화되지 않은 것일 수 있습니다 .

그러나 이것은 BeginAwaiter가 첫 번째 호출 후에 실제로 호출을 시작하지 않으며 이러한 경우 false를 리턴한다고 가정합니다. 물론 부작용이 있거나 단순히 다른 결과를 낼 수 있기 때문에 시작은 받아 들일 수 없습니다. 또한 EndAwaiter는 호출 횟수에 관계없이 항상 동일한 값을 리턴하며 BeginAwait가 false를 리턴 할 때 호출 될 수 있다고 가정합니다 (위의 가정에 따라).

경쟁 조건에 대한 경계 인 것처럼 보일 것입니다. 우리가 state = 0 이후에 다른 스레드에 의해 movenext가 호출되는 문장을 인라인하면 다음과 같이 보입니다.

this.<a1>t__$await2 = this.t1.GetAwaiter<int>();
this.<>1__state = 1;
this.$__doFinallyBodies = false;
this.<a1>t__$await2.BeginAwait(this.MoveNextDelegate)
this.<>1__state = 0;

//second thread
this.<a1>t__$await2 = this.t1.GetAwaiter<int>();
this.<>1__state = 1;
this.$__doFinallyBodies = false;
this.<a1>t__$await2.BeginAwait(this.MoveNextDelegate)
this.$__doFinallyBodies = true;
this.<>1__state = 0;
this.<1>t__$await1 = this.<a1>t__$await2.EndAwait();

//other thread
this.<1>t__$await1 = this.<a1>t__$await2.EndAwait();

위의 가정이 맞다면 sawiater를 얻고 같은 값을 <1> t __ $ await1에 다시 할당하는 등 불필요한 작업이 수행됩니다. 상태가 1로 유지되면 마지막 부분은 다음과 같습니다.

//second thread
//I suppose this un matched call to EndAwait will fail
this.<1>t__$await1 = this.<a1>t__$await2.EndAwait();

또한 2로 설정되면 상태 머신은 이미 첫 번째 조치의 값이 맞지 않은 것으로 가정하고 결과를 계산하는 데 (잠재적으로) 할당되지 않은 변수가 사용됩니다


답변

스택 / 중첩 비동기 호출과 관련이있을 수 있습니까? ..

즉 :

async Task m1()
{
    await m2;
}

async Task m2()
{
    await m3();
}

async Task m3()
{
Thread.Sleep(10000);
}

이 상황에서 movenext 델리게이트가 여러 번 호출됩니까?

그냥 펀 트야?


답변

실제 상태 설명 :

가능한 상태 :

  • 0 초기화 (그렇다고 생각) 또는 작업 종료를 기다리는 중
  • > 0 다음 상태를 선택하여 방금 MoveNext라고합니다.
  • -1 종료

이 구현은 어디에서나 (다음 대기 중) MoveNext에 대한 다른 호출이 처음부터 다시 전체 상태 체인 을 재평가하고 이미 오래된 시간에있을 수있는 결과를 재평가 하도록 보장하고 싶 습니까?


답변