Fire and forget async Task vs Task.Run

12,530

I just want to know why and what is the difference with fire-and-forgetting async Task and task returned from Task.Run

The difference is whether the AspNetSynchronizationContext is captured, which is the "request context" (including such things as the current culture, session state, etc).

When directly invoking, Start is run in that request context, and await by default will capture that context and resume on that context (more info on my blog). This can cause "interesting" behavior, if, say, Start attempts to resume on a request context for a request that has already been completed.

When using Task.Run, Start is run on a different thread pool thread that does not have a request context. So, await will not capture a context and will resume on any available thread pool thread.

I know you already know this, but I must reiterate for Googlers: Remember that any work queued this way is not reliable. At the very least, you should be using HostingEnvironment.QueueBackgroundWorkItem (or my AspNetBackgroundTasks library if you're not on 4.5.2 yet). These are both very similar; they will queue the background work to the thread pool (in fact, my library's BackgroundTaskManager.Run does call Task.Run), but both solutions will also take the extra step of registering that work with the ASP.NET runtime. This isn't enough to make the background work "reliable" in a true sense of the word, but it does minimize the chances that you'll lose the work.

Share:
12,530
bigbearzhu
Author by

bigbearzhu

Updated on June 06, 2022

Comments

  • bigbearzhu
    bigbearzhu almost 2 years

    I'm seeing some null reference issue when developing some ASP.Net application. The exception is as this below:

    Description: The process was terminated due to an unhandled exception.
    Exception Info: System.NullReferenceException
    Stack:
       at System.Threading.Tasks.AwaitTaskContinuation.<ThrowAsyncIfNecessary>b__1(System.Object)
       at System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object, Boolean)
       at System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object, Boolean)
       at System.Threading.QueueUserWorkItemCallback.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem()
       at System.Threading.ThreadPoolWorkQueue.Dispatch()
    

    After searching through Google and SO, it was very likely caused by some of my code fire and forget async Task in this fashion:

    public interface IRepeatedTaskRunner
    {
        Task Start();
        void Pause();
    }
    
    public class RepeatedTaskRunner : IRepeatedTaskRunner
    {
        public async Task Start() //this is returning Task
        {
            ...
        }
    }
    
    public class RepeatedTaskRunnerManager{
        private IRepeatedTaskRunner _runner;
        public void Foo(){
            _runner.Start(); //this is not awaited.
        }
    }
    

    I think it is very similar to this question "Fire and forget async method in asp.net mvc", but is not completely the same, as my code is not on controller and accepting requests. My code is dedicated for running background tasks.

    As the same in the above SO question, by changing the code to this below fixed the issue, but I just want to know why and what is the difference with fire-and-forgetting async Task and task returned from Task.Run:

    public interface IRepeatedTaskRunner
    {
        Task Start();
        void Pause();
    }
    
    public class RepeatedTaskRunner : IRepeatedTaskRunner
    {
        public Task Start() // async is removed.
        {
            ...
        }
    }
    
    public class RepeatedTaskRunnerManager{
        private IRepeatedTaskRunner _runner;
        public void Foo(){
            Task.Run(() => _runner.Start()); //this is not waited.
        }
    }
    

    From my point of view, both code just create one task and forget about it. The only difference is the first one there is no await on the Task. Would that cause the difference in behavior?

    I'm aware of the limitation and bad effect of the fire and forget pattern as from the other SO question.

  • bigbearzhu
    bigbearzhu about 8 years
    Excellent article on your blog! I think I did need read more tutorial like that before I started using those awaits, especially for the captured context part! And it possibly also explains why when I put those code in unit tests, it didn't throw. It is just because the capture context (I would think it is just the main thread) still exists, right?
  • Stephen Cleary
    Stephen Cleary about 8 years
    @bigbearzhu: Depends on your test framework. If you were using MSTest, then there is no context to capture in the first place. xUnit provides a context of its own, but it won't be disposed of until after the unit test completes.
  • bigbearzhu
    bigbearzhu about 8 years
    Indeed! SynchronizationContext.Current is null before the task is created (I'm using MSTest). Many thanks.
  • Brain2000
    Brain2000 over 7 years
    @StephenCleary Will adding the app key "aspnet:UseTaskFriendlySynchronizationContext" = "true" in the web.config file resolve the described "interesting" behavior where the thread resumes on a completed request, by first setting the correct request context?
  • Stephen Cleary
    Stephen Cleary over 7 years
    @Brain2000: You have to use UseTaskFriendlySynchronizationContext (or set targetFramework to 4.5 or higher), or else the behavior of await is completely undefined. It won't fix the situation where await captures a context that is then completed - the appropriate solution in that case is to fix the broken code.
  • Brain2000
    Brain2000 over 7 years
    @StephenCleary It sounds like you can never rely on the current.context to be correct after an await then. I thought I read somewhere in a blog (might have been one of your blogs!) that it restores the current.context for the return thread.
  • Stephen Cleary
    Stephen Cleary over 7 years
    @Brain2000: It does restore the context. This question is dealing with a situation where the await will attempt to return on a context for a request that has already completed. And the answer summary is pretty much "don't do that."
  • mcont
    mcont about 6 years
    Does the last paragraph for "googlers" apply when not using IIS with ASP.NET? I understand that the problem appears when the process gets shut down by IIS
  • Stephen Cleary
    Stephen Cleary about 6 years
    @MatteoContrini: A similar problem applies to any host: the host does not know when it's safe to shutdown or exit.