어제는 생성 된 코드가 어떻게 생겼는지에 특히 탐구에서, 기능 “비동기”새로운 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에 대한 다른 호출이 처음부터 다시 전체 상태 체인 을 재평가하고 이미 오래된 시간에있을 수있는 결과를 재평가 하도록 보장하고 싶 습니까?