How does compiler converts return value into return Task<value> in async methods?

老子叫甜甜 提交于 2019-12-31 07:41:49

问题


I've designed the following method to create records.

public Task<Guid> NotAwaited()
{
  Account account = new Account();
  Context.Accounts.Add(account);
  Context.SaveChangesAsync();
  return new Task<Guid>(() => account.Id);
}

Then, I realized that there's risk of the saving not being finished at the moment of returning the guid. So I've added await, which required me to decorate the method signature with async. Upon that, I got an error demanding a simpler syntax of what's being returned, like this.

public async Task<Guid> Awaited()
{
  Account account = new Account();
  Context.Accounts.Add(account);
  await Context.SaveChangesAsync();
  return account.Id;
}

I understand that the account.Id part gets converted to a task somehow. I'm just uncertain how. It feel like if it's black magic (which I understand it isn't).

Is there an implicit conversion? Or am I still performing the asynchronous call improperly?


回答1:


You can think of async as wrapping results (both return values and exceptions) into a Task<T>.

Likewise, await unwraps the results (extracting the return value or raising an exception).

I have an async intro that goes into more detail, and I recommend async best practices as followup to that. On a side note, you should never use the Task constructor.




回答2:


It feel like if it's black magic

It probably is sufficiently advanced to be indistinguishable from magic.

You write C# code that the compiler then splits into pieces that run together and creates a state machine that "moves forward" every time an awaited asynchronous task completes. If you debug your code, the debugger knows how to represent the "local variables" (which may actually be instance members on a state machine type) in the debugger and map to the line of your original source code.

So for your case, the code could look something like (created via sharplab, see this gist):

[AsyncStateMachine(typeof(<Awaited>d__0))]
public Task<Guid> Awaited()
{
    <Awaited>d__0 stateMachine = default(<Awaited>d__0);
    stateMachine.<>t__builder = AsyncTaskMethodBuilder<Guid>.Create();
    stateMachine.<>1__state = -1;
    AsyncTaskMethodBuilder<Guid> <>t__builder = stateMachine.<>t__builder;
    <>t__builder.Start(ref stateMachine);
    return stateMachine.<>t__builder.Task;
}

[StructLayout(LayoutKind.Auto)]
[CompilerGenerated]
private struct <Awaited>d__0 : IAsyncStateMachine
{
    public int <>1__state;

    public AsyncTaskMethodBuilder<Guid> <>t__builder;

    private Account <account>5__2;

    private TaskAwaiter <>u__1;

    private void MoveNext()
    {
        int num = <>1__state;
        Guid id;
        try
        {
            TaskAwaiter awaiter;
            if (num != 0)
            {
                <account>5__2 = new Account();
                Context.Accounts.Add(<account>5__2);
                awaiter = Context.SaveChangesAsync().GetAwaiter();
                if (!awaiter.IsCompleted)
                {
                    num = (<>1__state = 0);
                    <>u__1 = awaiter;
                    <>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref this);
                    return;
                }
            }
            else
            {
                awaiter = <>u__1;
                <>u__1 = default(TaskAwaiter);
                num = (<>1__state = -1);
            }
            awaiter.GetResult();
            id = <account>5__2.Id;
        }
        catch (Exception exception)
        {
            <>1__state = -2;
            <>t__builder.SetException(exception);
            return;
        }
        <>1__state = -2;
        <>t__builder.SetResult(id);
    }

    void IAsyncStateMachine.MoveNext()
    {
        //ILSpy generated this explicit interface implementation from .override directive in MoveNext
        this.MoveNext();
    }

    [DebuggerHidden]
    private void SetStateMachine(IAsyncStateMachine stateMachine)
    {
        <>t__builder.SetStateMachine(stateMachine);
    }

    void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
    {
        //ILSpy generated this explicit interface implementation from .override directive in SetStateMachine
        this.SetStateMachine(stateMachine);
    }
}

You can see that the party where SaveChangesAsync() is called is located in a different logical branch than where the access to the Id property is. The local variable account is now the <account>5__2 field on the generated struct. None of the identifier names used is actually a valid C# identifier, but is valid in the underlying IL language that is compiled to, the above code is a decompiled C#-ish representation of the code that is actually generated.

Calls to the Awaited() method will actually create new instances of the "hidden" <Awaited>d__0 struct (in debug mode it will be a class instead of a struct to support edit-and-continue) and use types of the async infrastructure to wire this state machine up and run it.

MoveNext() is called when starting the state machine but also each time that an awaited task completes (as a continuation). You can see that the last part sets the result to the id value, which is basically your return statement.

Noteworthy to mention is that there is also a try-catch around most of the code which wraps exceptions into the task result - so you can throw in your code (or code called from your code) and it ends up creating a failed task instead of an unhandled exception when the asynchronous part of the method is scheduled.



来源:https://stackoverflow.com/questions/56587496/how-does-compiler-converts-return-value-into-return-taskvalue-in-async-methods

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!