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


195

어제는 생성 된 코드가 어떻게 생겼는지에 특히 탐구에서, 기능 "비동기"새로운 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 # 컴파일러 또는 그와 같은 결함이라고 주장하려고 시도하지 않습니다. 나는 내가 놓친 미묘한 이유가 있는지 알아 내려고 노력하고있다. :)


7
VB 컴파일러는 비슷한 상태 머신을 생성합니다 (예상 여부는 모르지만 VB에는 이전에 반복자 블록이 없었습니다)
Damien_The_Unbeliever

1
@Rune : MoveNextDelegate는 MoveNext를 나타내는 델리게이트 필드입니다. 매번 웨이터에게 전달되는 새로운 액션을 피하기 위해 캐시됩니다.
Jon Skeet

5
대답은 다음과 같습니다. 이것이 CTP입니다. 팀의 고질적 인 비트 가이 문제를 해결하고 언어 설계가 검증되었습니다. 그리고 그들은 매우 놀랍게도 그렇게했습니다. 제공되는 구현 (MoveNext가 아닌 컴파일러의 구현)이 크게 다를 것으로 예상해야합니다. Eric 또는 Lucian은 여기에 깊은 것이 없으며 대부분의 경우 중요하지 않으며 아무도 눈치 채지 않은 행동 / 버그에 대한 대답을 다시 생각한다고 생각합니다. CTP이기 때문입니다.
Chris Burrows

2
@ 스틸가 : 방금 ildasm으로 확인했으며 실제로이 작업을 수행하고 있습니다.
Jon Skeet

3
@ JonSkeet : 아무도 대답을지지하는 방법에 주목하십시오. 우리 중 99 %가 대답이 제대로 들리는 지 실제로 알 수 없습니다.
the_drow

답변:


71

좋아, 나는 마침내 진정한 대답을 얻었다. 나는 그것을 스스로 해결했지만 팀의 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 전화를.

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


8
@ Shekhar_Pro : 예, goto입니다. 자동 생성 된 상태 머신에서 많은 goto 명령문을 보게 될 것입니다. :)
Jon Skeet

12
@Shekhar_Pro : 수동으로 작성된 코드 내에서 코드를 읽고 따르기가 어렵 기 때문입니다. 디 컴파일하는 사람과 같은 바보를 제외하고는 아무도 자동 생성 된 코드를 읽지 않습니다. :
Jon Skeet

그래서 무엇을 않는 우리가 예외 후 다시 기다리고 때 일이? 우리는 다시 시작?
구성자

1
@configurator : 대기 할 때 GetAwaiter를 호출합니다. 이것은 내가 기대하는 것입니다.
Jon Skeet

gotos가 항상 코드를 읽기 어렵게 만드는 것은 아닙니다. 사실, 때때로 그들은 사용하기에 합리적입니다 (사실 희생). 예를 들어, 때때로 여러 개의 중첩 루프를 끊어야 할 수도 있습니다. goto (및 IMO uglier 사용)의 덜 사용되는 기능은 스위치 문을 계단식으로 만드는 것입니다. 별도의 메모에서, 나는 고 토스가 일부 프로그래밍 언어의 주요 토대 였을 때와 고토에 대한 단순한 언급이 개발자들을 떨게하는 이유를 완전히 깨닫는 날과 나이를 기억합니다. 제대로 사용하지 않으면 추악하게 만들 수 있습니다.
벤 레시

5

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


상태가 실제로되지 않고 있음을 유념 사용 0으로 지정하고보다 의미있는 값으로 할당 사이. 경쟁 조건을 막으려면 MoveNext가 시작될 때 부적절한 사용을 감지하기 위해 -2와 같은 다른 값을 표시해야합니다. 어쨌든 한 번에 두 개의 스레드가 단일 인스턴스를 실제로 사용해서는 안된다는 점을 명심하십시오. 이는 항상 "일시 중지"하는 단일 동기 메소드 호출의 환영을주기위한 것입니다.
Jon Skeet

@Jon 비동기 조건에서 경쟁 조건에 문제가되지는 않지만 반복 블록에있을 수도 있고 남을 수도 있음
Rune FS

@Tony : 다음 CTP 나 베타가 나올 때까지 기다렸다가 그 동작을 확인합니다.
Jon Skeet

1

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

즉 :

async Task m1()
{
    await m2;
}

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

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

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

그냥 펀 트야?


이 경우 세 가지 다른 생성 클래스가 있습니다. MoveNext()각각에 대해 한 번 호출됩니다.
Jon Skeet

0

실제 상태 설명 :

가능한 상태 :

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

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


그러나 왜 처음부터 시작하고 싶습니까? 그것은 거의 확실입니다 하지 당신이 실제로 일어날 싶어 무엇을 - 다른 아무것도 있기 때문에 당신은, 예외가 던져 싶어 한다 MoveNext를 호출되지 않습니다.
Jon Skeet
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.