When is the best place to use Task.Result instead of awaiting Task

前端 未结 4 676
小鲜肉
小鲜肉 2020-12-31 19:09

Whilst I\'ve been using async code in .NET for a while, I\'ve only recently started to research it and understand what\'s going on. I\'ve just been going through my code an

相关标签:
4条回答
  • 2020-12-31 19:50

    Let's make sure to not bury the lede here:

    So for example: [some correct code] becomes [some incorrect code]

    NEVER NEVER NEVER DO THIS.

    Your instinct that you can restructure your control flow to improve performance is excellent and correct. Using Result to do so is WRONG WRONG WRONG.

    The correct way to rewrite your code is

    var userTask = _userRepo.GetByUsername(User.Identity.Name);    
    //Some work that doesn't rely on the user object    
    user = await _userRepo.UpdateLastAccessed(await userTask, DateTime.Now);    
    return user;
    

    Remember, await does not make a call asynchronous. Await simply means "if the result of this task is not yet available, go do something else and come back here after it is available". The call is already asynchronous: it returns a task.

    People seem to think that await has the semantics of a co-call; it does not. Rather, await is the extract operation on the task comonad; it is an operator on tasks, not call expressions. You normally see it on method calls simply because it is a common pattern to abstract away an async operation as a method. The returned task is the thing that is awaited, not the call.

    However, things I've seen posted imply that result should be used rarely and await is preferred but I don't understand why I'd want to wait for my user object to be fetched if I can be performing some other independent logic at the same time?

    Why do you believe that using Result will allow you to perform other independent logic at the same time??? Result prevents you from doing exactly that. Result is a synchronous wait. Your thread cannot be doing any other work while it is synchronously waiting for the task to complete. Use an asynchronous wait to improve efficiency. Remember, await simply means "this workflow cannot progress further until this task is completed, so if it is not complete, find more work to do and come back later". A too-early await can, as you note, make for an inefficient workflow because sometimes the workflow can progress even if the task is not complete.

    By all means, move around where the awaits happen to improve efficiency of your workflow, but never never never change them into Result. You have some deep misunderstanding of how asynchronous workflows work if you believe that using Result will ever improve efficiency of parallelism in the workflow. Examine your beliefs and see if you can figure out which one is giving you this incorrect intuition.

    The reason why you must never use Result like this is not just because it is inefficient to synchronously wait when you have an asynchronous workflow underway. It will eventually hang your process. Consider the following workflow:

    • task1 represents a job that will be scheduled to execute on this thread in the future and produce a result.
    • asynchronous function Foo awaits task1.
    • task1 is not yet complete, so Foo returns, allowing this thread to run more work. Foo returns a task representing its workflow, and signs up completing that task as the completion of task1.
    • The thread is now free to do work in the future, including task1.
    • task1 completes, triggering the execution of the completion of the workflow of Foo, and eventually completing the task representing the workflow of Foo.

    Now suppose Foo instead fetches Result of task1. What happens? Foo synchronously waits for task1 to complete, which is waiting for the current thread to become available, which never happens because we're in a synchronous wait. Calling Result causes a thread to deadlock with itself if the task is somehow affinitized to the current thread. You can now make deadlocks involving no locks and only one thread! Don't do this.

    0 讨论(0)
  • 2020-12-31 19:55

    In your case, you can use:

    user = await _userRepo.UpdateLastAccessed(await userTask, DateTime.Now);
    

    or perhaps more clearly:

    var user = await _userRepo.GetByUsername(User.Identity.Name);
    //Some work that doesn't rely on the user object
    user = await _userRepo.UpdateLastAccessed(user, DateTime.Now);
    

    The only time you should touch .Result is when you know the task has been completed. This can be useful in some scenarios where you are trying to avoid creating an async state machine and you think there's a good chance that the task has completed synchronously (perhaps using a local function for the async case), or if you're using callbacks rather than async/await, and you're inside the callback.

    As an example of avoiding a state machine:

    ValueTask<int> FetchAndProcess(SomeArgs args) {
        async ValueTask<int> Awaited(ValueTask<int> task) => SomeOtherProcessing(await task);
        var task = GetAsyncData(args);
        if (!task.IsCompletedSuccessfully) return Awaited(task);
        return new ValueTask<int>(SomeOtherProcessing(task.Result));
    }
    

    The point here is that if GetAsyncData returns a synchronously completed result, we completely avoid all the async machinery.

    0 讨论(0)
  • 2020-12-31 20:08

    Async await does not mean that several threads will be running your code.

    However, it will lower the time your thread will be waiting idly for processes to finish, thus finishing earlier.

    Whenever the thread normally would have to wait idly for something to finish, like waiting for a web page to download, a database query to finish, a disk write to finish, the async-await thread will not be waiting idly until the data is written / fetched, but looks around if it can do other things instead, and come back later after the awaitable task is finished.

    This has been described with a cook analogy in this inverview with Eric Lippert. Search somewhere in the middle for async await.

    Eric Lippert compares async-await with one(!) cook who has to make breakfast. After he starts toasting the bread he could wait idly until the bread is toasted before putting on the kettle for tea, wait until the water boils before putting the tea leaves in the teapot, etc.

    An async-await cook, wouldn't wait for the toasted bread, but put on the kettle, and while the water is heating up he would put the tea leaves in the teapot.

    Whenever the cook has to wait idly for something, he looks around to see if he can do something else instead.

    A thread in an async function will do something similar. Because the function is async, you know there is somewhere an await in the function. In fact, if you forget to program the await, your compiler will warn you.

    When your thread meets the await, it goes up its call stack to see if it can do something else, until it sees an await, goes up the call stack again, etc. Once everyone is waiting, he goes down the call stack and starts waiting idly until the first awaitable process is finished.

    After the awaitable process is finished the thread will continue processing the statements after the await until he sees an await again.

    It might be that another thread will continue processing the statements that come after the await (you can see this in the debugger by checking the thread ID). However this other thread has the context of the original thread, so it can act as if it was the original thread. No need for mutexes, semaphores, IsInvokeRequired (in winforms) etc. For you it seems as if there is one thread.

    Sometimes your cook has to do something that takes up some time without idly waiting, like slicing tomatoes. In that case it might be wise to hire a different cook and order him to do the slicing. In the mean time your cook can continue with the eggs that just finished boiling and needed peeling.

    In computer terms this would be if you had some big calculations without waiting for other processes. Note the difference with for instance writing data to disk. Once your thread has ordered that the data needs to be written to disk, it normally would wait idly until the data has been written. This is not the case when doing big calculations.

    You can hire the extra cook using Task.Run

    async Task<TimeSpan> CalculateSunSet()
    {
        // start fetching sunset data. however don't wait for the result yet
        // you've got better things to do:
        Task<SunsetData> taskFetchData = FetchSunsetData();
    
        // because you are not awaiting your thread will do the following:
        Location location = FetchLocation();
    
        // now you need the sunset data, start awaiting for the Task:
        SunsetData sunsetData = await taskFetchData;
    
        // some big calculations are needed, that take 33 seconds,
        // you want to keep your caller responsive, so start a Task
        // this Task will be run by a different thread:
        ask<DateTime> taskBigCalculations = Taks.Run( () => BigCalculations(sunsetData, location);
    
        // again no await: you are still free to do other things
        ...
        // before returning you need the result of the big calculations.
        // wait until big calculations are finished, keep caller responsive:
        DateTime result = await taskBigCalculations;
        return result;
    }
    
    0 讨论(0)
  • 2020-12-31 20:11

    Have you considered this version?

    var userTask = _userRepo.GetByUsername(User.Identity.Name);
    
    //Some work that doesn't rely on the user object
    
    user = await _userRepo.UpdateLastAccessed(await userTask, DateTime.Now);
    
    return user;
    

    This will execute the "work" while the user is retrieved, but it also has all the advantages of await that are described in Await on a completed task same as task.Result?


    As suggested you can also use a more explicit version to be able to inspect the result of the call in the debugger.

    var userTask = _userRepo.GetByUsername(User.Identity.Name);
    
    //Some work that doesn't rely on the user object
    
    user = await userTask;
    user = await _userRepo.UpdateLastAccessed(user, DateTime.Now);
    
    return user;
    
    0 讨论(0)
提交回复
热议问题