5

I think I'm not understanding something. I had thought that Task.Yield() forced a new thread/context to be started for a task but upon re-reading this answer it seems that it merely forces the method to be async. It will still be on the same context.

What's the correct way - in an asp.net process - to create and run multiple tasks in parallel without causing deadlock?

In other words, suppose I have the following method:

async Task createFileFromLongRunningComputation(int input) { 
    //many levels of async code
}

And when a certain POST route is hit, I want to simultaneously launch the above methods 3 times, return immediately, but log when all three are done.

I think I need to put something like this into my action

public IHttpAction Post() {
   Task.WhenAll(
       createFileFromLongRunningComputation(1),
       createFileFromLongRunningComputation(2),
       createFileFromLongRunningComputation(3)
   ).ContinueWith((Task t) =>
      logger.Log("Computation completed")
   ).ConfigureAwait(false);
   return Ok();

}

What needs to go into createFileFromLongRunningComputation? I had thought Task.Yield was correct but it apparently is not.

Community
  • 1
  • 1
George Mauer
  • 117,483
  • 131
  • 382
  • 612

2 Answers2

7

The correct way to offload concurrent work to different threads is to use Task.Run as rossipedia suggested.

The best solutions for background processing in ASP.Net (where your AppDomain can be recycled/shut down automatically together with all your tasks) are in Scott Hanselman and Stephen Cleary's blogs (e.g. HangFire)

However, you could utilize Task.Yield together with ConfigureAwait(false) to achieve the same.

All Task.Yield does is return an awaiter that makes sure the rest of the method doesn't proceed synchronously (by having IsCompleted return false and OnCompleted execute the Action parameter immediately). ConfigureAwait(false) disregards the SynchronizationContext and so forces the rest of the method to execute on a ThreadPool thread.

If you use both together you can make sure an async method returns a task immediately which will execute on a ThreadPool thread (like Task.Run):

async Task CreateFileFromLongRunningComputation(int input)
{
    await Task.Yield().ConfigureAwait(false);
    // executed on a ThreadPool thread
}

Edit: George Mauer pointed out that since Task.Yield returns YieldAwaitable you can't use ConfigureAwait(false) which is a method on the Task class.

You can achieve something similar by using Task.Delay with a very short timeout, so it wouldn't be synchronous but you wouldn't waste much time:

async Task CreateFileFromLongRunningComputation(int input)
{
    await Task.Delay(1).ConfigureAwait(false);
    // executed on a ThreadPool thread
}

A better option would be to create a YieldAwaitable that simply disregards the SynchronizationContext the same as using ConfigureAwait(false) does:

async Task CreateFileFromLongRunningComputation(int input)
{
    await new NoContextYieldAwaitable();
    // executed on a ThreadPool thread
}

public struct NoContextYieldAwaitable
{
    public NoContextYieldAwaiter GetAwaiter() { return new NoContextYieldAwaiter(); }
    public struct NoContextYieldAwaiter : INotifyCompletion
    {
        public bool IsCompleted { get { return false; } }
        public void OnCompleted(Action continuation)
        {
            var scheduler = TaskScheduler.Current;
            if (scheduler == TaskScheduler.Default)
            {
                ThreadPool.QueueUserWorkItem(RunAction, continuation);
            }
            else
            {
                Task.Factory.StartNew(continuation, CancellationToken.None, TaskCreationOptions.PreferFairness, scheduler);
            }
        }

        public void GetResult() { }
        private static void RunAction(object state) { ((Action)state)(); }
    }
}

This isn't a recommendation, it's an answer to your Task.Yield questions.

i3arnon
  • 113,022
  • 33
  • 324
  • 344
  • This is the correct answer to the _actual question_. My answer has more discussion points about whether or not the OPs approach is a good one. – rossipedia Jan 16 '15 at 16:16
  • 2
    @rossipedia indeed. And I want to reiterate, this isn't how real code should look like. – i3arnon Jan 16 '15 at 16:18
  • Oy vey, now gotta figure out if I can convince infra to deploy 4.5.2 – George Mauer Jan 16 '15 at 16:25
  • `Task.Yield()` does not return a Task and therefore there is no `ConfigureAwait` function. So the not-recommended approach doesn't work. – George Mauer Jan 16 '15 at 17:58
  • @GeorgeMauer you're right, I haven't thought about. I added a really interesting solution to the answer. – i3arnon Jan 16 '15 at 18:51
  • Interesting, any particular reason why `NoContextYieldAwaiter` is a struct? – George Mauer Jan 16 '15 at 18:54
  • @GeorgeMauer Yes. It's a struct because `YieldAwaiter` (and `TaskAwaiter` are structs). And they are structs for optimization reasons, mainly to reduce heap allocations. – i3arnon Jan 16 '15 at 19:03
4

(l3arnon's answer is the correct one. This answer is more of a discussion on whether the approach posed by the OP is a good one.)

You don't need anything special, really. The createFileFromLongRunningComputation method doesn't need anything special, just make sure you are awaiting some async method in it and the ConfigureAwait(false) should avoid the deadlock, assuming you're not doing anything out of the ordinary (probably just file I/O, given the method name).

Caveat:

This is risky. ASP.net will most likely pull the rug out from under you in this situation if the tasks take too long to finish.

As one of the commenters pointed out, there are better ways of accomplishing this. One of them is HostingEnvironment.QueueBackgroundWorkItem (which is only available in .NET 4.5.2 and up).

If the long running computation takes a significantly long time to complete, you're probably better off keeping it out of ASP.net entirely. In that situation, a better method would be to use some sort of message queue, and a service that processes those messages outside of IIS/ASP.net.

Community
  • 1
  • 1
rossipedia
  • 56,800
  • 10
  • 90
  • 93
  • But there's certain situations in asp.net where that will cause a deadlock when await is used. Also, according to the linked answer, this might still run synchronously. – George Mauer Jan 16 '15 at 15:52
  • As long as he's got that `ConfigureAwait(false)`, and doesn't attempt to access the `HttpContext` from the request, he _should_ be ok. – rossipedia Jan 16 '15 at 15:54
  • @rossipedia are you aware that you're not actually waiting for these tasks to complete? – i3arnon Jan 16 '15 at 15:56
  • @l3arnon notice that I do not want to wait for the tasks to complete, I want to return immediately. What I'm wary about this is what about the comment that `Task.Run` *might* run synchronously – George Mauer Jan 16 '15 at 15:56
  • 1
    @GeorgeMauer Then why is the title 'await multiple tasks". Also, this is very risky. If you want to have background processing in asp.net there are better ways. – i3arnon Jan 16 '15 at 15:58
  • @l3arnon Yup. OP stated he wants to return immediately, ie: not wait for those tasks to finish. They will finish on a ThreadPool thread. I've used the technique successfully quite a few times. – rossipedia Jan 16 '15 at 16:00
  • Given that, you make a good point. There are definitely better ways. – rossipedia Jan 16 '15 at 16:00
  • Thanks @l3arnon I fixed up the title. What do you mean by better ways to run background tasks? – George Mauer Jan 16 '15 at 16:01
  • 1
    @GeorgeMauer all you need is here: http://www.hanselman.com/blog/HowToRunBackgroundTasksInASPNET.aspx – i3arnon Jan 16 '15 at 16:02
  • Can I ask why you've wrapped the `createFileFromLongRunningComputation` calls in `Task.Run`? I've just tried it without and it seems to behave the same, but will freely admit I don't 100% know what I'm doing. – Rawling Jan 16 '15 at 16:04
  • 2
    Also related - http://blog.stephencleary.com/2014/06/fire-and-forget-on-asp-net.html – Daniel Kelley Jan 16 '15 at 16:04
  • @Rawling the actual use case is the typical REST pattern where you POST a resource that is going to kick off some background computation, get back an id, and later are able to GET aspects of that resource by id. `POST /CreateReport` returns back id 12345 and kicks off processing. Later you can do `GET /Report/12345?pageStart=5&pageEnd=50` – George Mauer Jan 16 '15 at 16:09
  • 2
    @GeorgeMauer: You really should not be doing this in ASP.NET, then. You should have a persistent queue with an independent backend worker process. – Stephen Cleary Jan 16 '15 at 16:11
  • 2
    He already has asynchronous tasks. There is no reason to wrap them in calls to `Run`; it's just wasting time scheduling a task in the thread pool to *start* an asynchronous operation. – Servy Jan 16 '15 at 16:11
  • 1
    Thanks @StephenCleary I imagine your Fire And Forget article covers this? I'm wary to introduce a ton of new infrastructure for just this one feature. – George Mauer Jan 16 '15 at 16:12
  • 1
    @GeorgeMauer: Yes. You can always `await` them before returning to the caller if you want to keep it in ASP.NET. If you want to return "early", then you'll need an appropriate architecture to support that requirement. – Stephen Cleary Jan 16 '15 at 16:14
  • @Servy You're absolutely right. It's early here, I need more coffee. – rossipedia Jan 16 '15 at 16:14