0

In an ASP.NET application, I have an action which when hit, starts a new background task in the following way:

Task.Factory.StartNew(async () => await somethingWithCpuAndIo(input), CancellationToken.None, TaskCreationOptions.DenyChildAttach | TaskCreationOptions.LongRunning, TaskScheduler.FromCurrentSynchronizationContext());

I'm not awaiting it, I just want it to start, and continuing doing its work in the background. Immediately after that, I return a response to the client.

For some reason though, after the initial thread executing the background work hits an await that awaits the completion of a method, when debugging, I successfully resolve the method, but upon returning, execution just stops and does not continue below that point.

Interestingly, if I fully await this task (using double await), everything works as expected.

Is this due to the SynchronizationContext? The moment I return a response, the synchronizationContext is disposed/removed? (The SynchronizationContext is being used inside the method)

If it is due to that, where exactly does the issue happen?

A) When the Scheduler attempts to assign the work on the given synchronizationContext, it will already be disposed, so nothing will be provided

B) Somewhere down the lines in the method executing, when I return a response to the client, the synchronizationContext is lost, regardless of anything else.

C) Something else entirely?

If it's A), I should be able to fix this by simply doing Thread.Sleep() between scheduling the work and returning a response. (Tried that, it didn't work.)

If it's B) I have no idea how I can resolve this. Help will be appreciated.

SpiritBob
  • 2,355
  • 3
  • 24
  • 62
  • 1
    `Task.Factory.StartNew(async () =>` returns a `Task`. The outer task just creates the inner task. Creating a task is usually a not-long-running job. – Theodor Zoulias Dec 04 '19 at 02:57
  • 1
    Here is an interesting reading: [How to run Background Tasks in ASP.NET](https://www.hanselman.com/blog/HowToRunBackgroundTasksInASPNET.aspx). And here is another one: [Fire and Forget on ASP.NET](https://blog.stephencleary.com/2014/06/fire-and-forget-on-asp-net.html). – Theodor Zoulias Dec 04 '19 at 09:29
  • @TheodorZoulias meaning we should never pass `Task.LongRunning` when using an async lambda? – SpiritBob Dec 04 '19 at 09:52
  • "Never" is a strong word. Normally you should have [no reason](http://blog.i3arnon.com/2015/07/02/task-run-long-running/) to use the `TaskCreationOptions.LongRunning` option with `async` lambdas, but there might be some rare exceptions. Generally this option is intended to be used as an optimization technique, which means ["don't use it unless you find that you really need it"](https://social.msdn.microsoft.com/Forums/en-US/8304b44f-0480-488c-93a4-ec419327183b/when-should-a-taks-be-considered-longrunning?forum=parallelextensions). – Theodor Zoulias Dec 04 '19 at 10:11

2 Answers2

3

As Gabriel Luci has pointed out, it is due the the first awaited incomplete Task returning immediately, but there's a wider point to be made about Task.Factory.StartNew.

Task.Factory.StartNew should not be used with async code, and neither should TaskCreationOptions.LongRunning. TaskCreationOptions.LongRunning should be used for scheduling long running CPU-bound work. With an async method, it may be logically long running, but Task.Factory.StartNew is about starting synchronous work, the synchronous part of an async method is the bit before the first await, this is usually very short.

Here is the guidance from David Fowler (Partner Software Architect at Microsoft on the ASP.NET team) on the matter: https://github.com/davidfowl/AspNetCoreDiagnosticScenarios/blob/86b502e88c752e42f68229afb9f1ac58b9d1fef7/AsyncGuidance.md#avoid-using-taskrun-for-long-running-work-that-blocks-the-thread

See the 3rd bulb:

Don't use TaskCreationOptions.LongRunning with async code as this will create a new thread which will be destroyed after first await.

Stuart
  • 5,358
  • 19
  • 28
  • I'm using `Task.Factory.StartNew` in order to pass the synchronizationContext to the work, via `TaskScheduler.FromCurrentSynchronizationContext`. If I use `Task.Run`, the default Task Scheduler is used, which is the Thread pool. – SpiritBob Dec 04 '19 at 08:28
  • If you want to use the current `SynchronizationContext`, what are you trying to do? I'm struggling to think of a scenario where you'd want that. @TheodorZoulias I use thread-bound and CPU-bound interchangeably, should I avoid that term? – Stuart Dec 04 '19 at 08:41
  • It's an ASP.NET application. I'm scheduling something big that if awaited in the request, will timeout the request itself on the client's end. To avoid that, the best idea is to schedule the big work, and then signal/send its result over other means. Inside the long work, there are parts where I'm accessing `HttpContext.Current`. I need that, otherwise the work will throw an exception at that point of a null reference. – SpiritBob Dec 04 '19 at 08:45
  • 1
    I think that the term [CPU-bound](https://stackoverflow.com/questions/868568/what-do-the-terms-cpu-bound-and-i-o-bound-mean) is more established. TBH I've never encountered the term thread-bound before. – Theodor Zoulias Dec 04 '19 at 09:11
  • @SupportMonica-SpiritBob I think that will be problematic, if you need the work to happen asynchronous to the web request you'll want to not touch `HttpContext` in that later work. I would copy the values off of `HttpContext.Current` to need and pass them in. – Stuart Dec 04 '19 at 09:17
  • @Stuart yeah, that's a potential solution I was aware of, but the refactoring of the code will be a pain. Thank you still! – SpiritBob Dec 04 '19 at 09:25
1

Your comments made you intentions a little clearer. What I think you want to do is:

  1. Start the task and don't wait for it. Return a response to the client before the background task completes.
  2. Make sure that the somethingWithCpuAndIo method has access to the request context.

But,

  1. A different thread won't be in the same context, and
  2. As soon as the first await is hit, a Task is returned, which also means that Task.Factory.StartNew returns and execution of the calling method continues. That means that the response is returned to the client. When the request completes, the context is disposed.

So you can't really do both things you want. There are a couple of ways to work around this:

First, you might be able to not start it on a different thread at all. This depends on when somethingWithCpuAndIo needs access to the context. If it only needs the context before the first await, then you can do something like this:

public IActionResult MyAction(input) {
    somethingWithCpuAndIo(input); //no await
}

private async Task somethingWithCpuAndIo(SomeThing input) {

    // You can read from the request context here

    await SomeIoRequest().ConfigureAwait(false);

    // Everything after here will run on a ThreadPool thread with no access
    // to the request context.
}

Every asynchronous method starts running synchronously. The magic happens when await is given an incomplete Task. So in this example, somethingWithCpuAndIo will start executing on the same thread, in the request context. When it hits the await, a Task is returned to MyAction, but it is not awaited, so MyAction completes executing and a response gets sent to the client before SomeIoRequest() has completed. But ConfigureAwait(false) tells it that we don't need to resume execution in the same context, so somethingWithCpuAndIo resume execution on a ThreadPool thread.

But that will only help you if you don't need the context after the first await in somethingWithCpuAndIo.

Your best option is to still execute on a different thread, but pass the values you need from the context into somethingWithCpuAndIo.

But also, use Task.Run instead of Task.Factory.StartNew for reasons described in detail here.

Update: This can very likely cause unpredictable results, but you can also try passing a reference to HttpContext.Current to the thread and setting HttpContext.Current in the new thread, like this:

var ctx = HttpContext.Current;
Task.Run(async () => {
    HttpContext.Current = ctx;
    await SomeIoRequest();
});

However, it all depends on how you are using the context. HttpContext itself doesn't implement IDiposable, so it, itself, can't be disposed. And the garbage collector won't get rid of it as long as you're holding a reference to it. But the context isn't designed to live longer than the request. So after the response is returned to the client, there may be many parts of the context that are disposed or otherwise unavailable. Test it out an see what explodes. But even if nothing explodes right now, you (or someone else) might come back to that code later, try to use something else in the context and get really confused when it blows up. It could make for some difficult-to-debug scenarios.

Gabriel Luci
  • 38,328
  • 4
  • 55
  • 84
  • Thank you Gabriel! I'm not sure if you saw it in my answer, but the application is ASP.NET. I remember reading something somewhere stating that any thread spawned is by default considered a background thread, meaning that when the main application stops, all background threads are automatically killed. If it's a foreground thread, it won't. But how does that logic apply to ASP.NET request thread? The moment the request thread returns, ASP.NET terminates any background threads, is that what's going on here? I really just want to start something in the background, and return a respns. – SpiritBob Dec 04 '19 at 08:24
  • If that's not possible, is there a way to make it possible? I even tried storing the task in a static list, which I think worked, but the synchronizationContext was lost. If you could help me out here, and validate my words, that'd be great! **TL;DR** Is there a way to schedule work to be done, after which we return a response to the client issuing the request, but that work to still be able to fetch the synchronizationContext for the handled request (and continue doing it's stuff)? – SpiritBob Dec 04 '19 at 08:26
  • Sorry, I missed that you mentioned ASP.NET. But you comments do make it a bit clearer. I rewrote my answer. – Gabriel Luci Dec 04 '19 at 13:48
  • Thank you for the insight! Unfortunately reading from the context indeed happens after a couple of `await` calls. The only reason I'm using `Task.Factory.StartNew` is to specifically allow the work to have access to the synchronizationContext. (`Task.Run` uses the default Task scheduler, which is the thread pool -- threads won't have the context I need them to have) My understanding is that if I pass `TaskScheduler.FromCurrentSynchronizationContext`, the thread that initially starts the work will have that synchronizationContext. (To preserve it, we should avoid `ConfigureAwait(false)`) – SpiritBob Dec 04 '19 at 14:55
  • Correct me if I'm wrong, but the thread that initially starts will be part of the thread pool, regardless if we used `TaskScheduler.FromCurrentSynchronizationContext` or not (as with any other threads that decide to resume the work.) On another note, is there a way to start a thread (Instead of a task) and somehow seemingly preserve the synchronizationContext, rather than refactoring tons of code/method calls to include the context as a parameter? – SpiritBob Dec 04 '19 at 14:57
  • Not really "preserve the synchronizationContext", but you can just manually set `HttpContext.Current`. I added that at the end of my answer. Results might be unpredictable though. – Gabriel Luci Dec 04 '19 at 15:31
  • 1
    Regarding setting `HttpContext.Current`, Stephen Cleary would probably disapprove: *HttpContext.Current shouldn't be assigned to, ever.* ([citation](https://stackoverflow.com/questions/58998460/async-web-service-call-not-consistently-asynchronous/59003089#59003089)) – Theodor Zoulias Dec 04 '19 at 15:42
  • 1
    @TheodorZoulias I agree. I wouldn't do it either. – Gabriel Luci Dec 04 '19 at 15:45