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이 실제로 그럴만한 이유가 있음을 확인한 후에야 해결되었습니다. 그에게 많은 감사를 전합니다- 그의 블로그 를 방문하십시오 .
The value 0 here is only special because it's not a valid state which you might be in just before the await
in a normal case. In particular, it's not a state which the state machine may end up testing for elsewhere. I believe that using any non-positive value would work just as well: -1 isn't used for this as it's logically incorrect, as -1 normally means "finished". I could argue that we're giving an extra meaning to state 0 at the moment, but ultimately it doesn't really matter. The point of this question was finding out why the state is being set at all.
The value is relevant if the await ends in an exception which is caught. We can end up coming back to the same await statement again, but we mustn't be in the state meaning "I'm just about to come back from that await" as otherwise all kinds of code would be skipped. It's simplest to show this with an example. Note that I'm now using the second CTP, so the generated code is slightly different to that in the question.
Here's the async method:
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;
}
Conceptually, the SimpleAwaitable
can be any awaitable - maybe a task, maybe something else. For the purposes of my tests, it always returns false for IsCompleted
, and throws an exception in GetResult
.
Here's the generated code for 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);
}
I had to move Label_ContinuationPoint
to make it valid code - otherwise it's not in the scope of the goto
statement - but that doesn't affect the answer.
Think about what happens when GetResult
throws its exception. We'll go through the catch block, increment i
, and then loop round again (assuming i
is still less than 3). We're still in whatever state we were before the GetResult
call... but when we get inside the try
block we must print "In Try" and call GetAwaiter
again... and we'll only do that if state isn't 1. Without the state = 0
assignment, it will use the existing awaiter and skip the Console.WriteLine
call.
It's a fairly tortuous bit of code to work through, but that just goes to show the kinds of thing that the team has to think about. I'm glad I'm not responsible for implementing this :)
if it was kept at 1 (first case) you would get a call to EndAwait
without a call to BeginAwait
. If it's kept at 2 (second case) you'd get the same result just on the other awaiter.
I'm guessing that calling the BeginAwait returns false if it has be started already (a guess from my side) and keeps the original value to return at the EndAwait. If that's the case it would work correctly whereas if you set it to -1 you might have an uninitialized this.<1>t__$await1
for the first case.
This however assumes that BeginAwaiter won't actually start the action on any calls after the first and that it will return false in those cases. Starting would of course be unacceptable since it could have side effect or simply give a different result. It also assumpes that the EndAwaiter will always return the same value no matter how many times it's called and that is can be called when BeginAwait returns false (as per the above assumption)
It would seem to be a guard against race conditions If we inline the statements where movenext is called by a different thread after the state = 0 in questions it woule look something like the below
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();
If the assumptions above are correct the there's some unneeded work done such as get sawiater and reassigning the same value to <1>t__$await1. If the state was kept at 1 then the last part would in stead be:
//second thread
//I suppose this un matched call to EndAwait will fail
this.<1>t__$await1 = this.<a1>t__$await2.EndAwait();
further if it was set to 2 the state machine would assume it already had gotten the value of the first action which would be untrue and a (potentially) unassigned variable would be used to calculate the result
Could it be something to do with stacked/nested async calls ?..
i.e:
async Task m1()
{
await m2;
}
async Task m2()
{
await m3();
}
async Task m3()
{
Thread.Sleep(10000);
}
Does the movenext delegate get called multiple times in this situation ?
Just a punt really?
Explanation of actual states:
possible states:
- 0 Initialized (i think so) or waiting for end of operation
- >0 just called MoveNext, chosing next state
- -1 ended
Is it possible that this implementation just wants to assure that if another Call to MoveNext from whereever happens (while waiting) it will reevaluate the whole state-chain again from the beginning, to reevaluate results which could be in the mean time already outdated?
'Programming' 카테고리의 다른 글
다른 데이터 구조 대신 배열을 사용하는 이유는 무엇입니까? (0) | 2020.05.12 |
---|---|
Wildfly의 Spring Security : 필터 체인 실행 중 오류 (0) | 2020.05.12 |
PHP에서 변수 이름을 문자열로 얻는 방법은 무엇입니까? (0) | 2020.05.12 |
Flux 앱에서 Ajax 요청은 어디에서해야합니까? (0) | 2020.05.12 |
모바일 장치에서 전화 번호로 전화하기 위해 하이퍼 링크를 만드는 방법은 무엇입니까? (0) | 2020.05.12 |