3

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.

Community
  • 1
  • 1
bigbearzhu
  • 2,381
  • 6
  • 29
  • 44
  • 1
    Code that causes the problem is not shown in the post... But caller of your `Foo` method does not provide correct synchronization context (by using default ASP.Net one) which tries to return to current request's thread. – Alexei Levenkov Mar 08 '16 at 02:15
  • I have put more comments in the code. The Start function on _runner was not awaited, which caused the issue. I have also changed the code to match more with my code structure. – bigbearzhu Mar 08 '16 at 02:26
  • @AlexeiLevenkov what do you mean "the correct synchronization context(by using default ASP.Net one)"? Can you provide me some example of that? Thanks. – bigbearzhu Mar 08 '16 at 03:00
  • On a conceptual level, Async/Await is antithetical to "Fire and Forget" threading model, which explicitly rejects any awaiting. Even though async/await can be used to implement "Fire and Forget" functionality, but there is a better option of using Task.Run(...) for this particular purpose. Best regards, – Alexander Bell Mar 08 '16 at 03:39
  • @AlexBell thanks, as I stated in my question, I knew that Task.Run is better option, but I just don't understand why using async Task here would cause the null reference exception. Would be good if anyone could give clearer explanation. – bigbearzhu Mar 08 '16 at 04:51
  • 2
    First `await` captures current synchronization context (see http://stackoverflow.com/questions/16827864/how-to-forget-synchronization-context-in-async-methods-in-c-sharp to attempts to avoid that - i.e. `Task.Run` is good option). For methods that must not return to request's thread you need to force using context that does not call back on request's thread when execution finished (as that thread is likely running other request by that time or gone). Also check out http://blogs.msdn.com/b/pfxteam/archive/2012/02/02/await-synchronizationcontext-and-console-apps-part-3.aspx (and part 1/2). – Alexei Levenkov Mar 08 '16 at 17:30
  • @AlexeiLevenkov thanks for your effort to answer my question, it helped me to understand the reason behind my question and reading Stephen Cleary's blog post: http://blog.stephencleary.com/2012/02/async-and-await.html – bigbearzhu Mar 08 '16 at 23:57

1 Answers1

17

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.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • 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? – bigbearzhu Mar 08 '16 at 23:53
  • @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. – Stephen Cleary Mar 09 '16 at 12:42
  • Indeed! SynchronizationContext.Current is null before the task is created (I'm using MSTest). Many thanks. – bigbearzhu Mar 09 '16 at 23:38
  • @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? – Brain2000 Oct 27 '16 at 17:50
  • @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. – Stephen Cleary Oct 27 '16 at 18:21
  • @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. – Brain2000 Oct 27 '16 at 18:24
  • @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." – Stephen Cleary Oct 27 '16 at 18:32
  • 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 – mcont Feb 27 '18 at 22:32
  • @MatteoContrini: A similar problem applies to any host: the host does not know when it's safe to shutdown or exit. – Stephen Cleary Feb 28 '18 at 00:21