29

I need a way to set an async task as long running without using Task.Factory.StartNew(...) and instead using Task.Run(...) or something similar.

Context:

I have Task that loops continuously until it is externally canceled that I would like to set as 'long running' (i.e. give it a dedicated thread). This can be achieved through the code below:

var cts = new CancellationTokenSource();
Task t = Task.Factory.StartNew(
async () => {
while (true)
{
    cts.Token.ThrowIfCancellationRequested();
    try
    {
        "Running...".Dump();
        await Task.Delay(500, cts.Token);
    }
    catch (TaskCanceledException ex) { }
} }, cts.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default);

The problem is that Task.Factory.StartNew(...) does not return the active async task that is passed in but rather a 'task of running the Action' which functionally always has taskStatus of 'RanToCompletion'. Since my code needs to be able to track the task's status to see when it becomes 'Canceled' (or 'Faulted') I need to use something like below:

var cts = new CancellationTokenSource();
Task t = Task.Run(
async () => {
while (true)
{
    cts.Token.ThrowIfCancellationRequested();
    try
    {
        "Running...".Dump();
        await Task.Delay(500, cts.Token);
    }
    catch (TaskCanceledException ex) { }
} }, cts.Token);

Task.Run(...), as desired, returns the async process itself allowing me to obtain actual statuses of 'Canceled' or 'Faulted'. I cannot specify the task as long running, however. So, anyone know how to best go about running an async task while both storing that active task itself (with desired taskStatus) and setting the task to long running?

quetzalcoatl
  • 32,194
  • 8
  • 68
  • 107
Ryan
  • 579
  • 1
  • 6
  • 15
  • Have you tried `StartNew` without having an `async` lambda? – Ben Voigt Nov 14 '14 at 00:51
  • @BenVoigt You can't `await` a `Task.Delay` in a non-async lambda. – Asad Saeeduddin Nov 14 '14 at 00:52
  • @BenVoigt Yeah, exactly. I need to 'await' on Task.Delay() so that the cancellation request will break the task out of that delay. – Ryan Nov 14 '14 at 00:53
  • @Asad: I'm pretty sure that `Task.Delay` is a placeholder for code doing computation in his real program. You wouldn't dedicate a background thread for calls to `Delay`. – Ben Voigt Nov 14 '14 at 00:54
  • @Ryan: Seriously, you have a task doing nothing but cancellable delay, and you're arranging for it to have its own thread?!? Anyway, if it has its own thread, `await` and `async` don't have any other code to run. So you can use `Task.Delay(500, cts.Token).Wait();` or [`cts.WaitHandle.WaitOne(TimeSpan.FromMilliseconds(500));`](http://stackoverflow.com/a/23633151/103167) – Ben Voigt Nov 14 '14 at 00:55
  • @BenVoigt You can substitute the `Task.Delay` for a more useful, asynchronous, `Task` returning operation, and the argument still holds. You can't yield and await a `Task` inside a non-async lambda (or method/delegate/whatever). – Asad Saeeduddin Nov 14 '14 at 00:56
  • @Asad: It's got a dedicated thread, so there's nothing to yield to. You don't yield and await in a dedicated thread. You use a synchronous programming model. – Ben Voigt Nov 14 '14 at 00:57
  • @BenVoigt The delay is actually used in the code when queries are not returning anything valid. We want to be querying as fast as possible when items are being returned, but otherwise ramp up the delay to decrease queries/s when there is down time. – Ryan Nov 14 '14 at 00:59
  • @Ryan But if you want a dedicated thread (which I missed, apologies Ben) why are you using an **asynchronous** wait? Just use `Thread.Sleep` and remove the `async` from your lambda. – Asad Saeeduddin Nov 14 '14 at 01:00
  • @Asad: `Thread.Sleep` isn't right either, unless there's an overload accepting a CancellationToken I missed. But I mentioned two ways to do cancellable sleep. – Ben Voigt Nov 14 '14 at 01:01
  • @Asad Substituting Task.Delay with Thread.Sleep allows me to get around this, but then when cts.Cancel() is called it will not break out of that Sleep, while Task.Delay(..., cts.Token) will break out upon cts.Cancel. – Ryan Nov 14 '14 at 01:05
  • @Ryan Ben's answer is the correct one here. I misunderstood the code and thought the task only needed to be cancelled between iterations. – Asad Saeeduddin Nov 14 '14 at 01:07
  • @BenVoigt I'm sorry for the necro-reply, but did I understand your point correctly -> By supplying TaskOptions.LongRunning, we spin our own OS thread. If we have our own thread, we don't need to use async/await, but rather wait on the tasks? I know that awaiting a normal task doesn't block and recycles the thread until the work is done ([There is no thread.](https://blog.stephencleary.com/2013/11/there-is-no-thread.html)). What happens for a thread that's long-running? Is this why we avoid awaits there - because they are effectively waiting on the job - they can't be recycled/unblocked? – SpiritBob Oct 31 '19 at 11:10

6 Answers6

32

I have Task that loops continuously until it is externally canceled that I would like to set as 'long running' (i.e. give it a dedicated thread)... anyone know how to best go about running an async task while both storing that active task itself (with desired taskStatus) and setting the task to long running?

There's a few problems with this. First, "long running" does not necessarily mean a dedicated thread - it just means that you're giving the TPL a hint that the task is long-running. In the current (4.5) implementation, you will get a dedicated thread; but that's not guaranteed and could change in the future.

So, if you need a dedicated thread, you'll have to just create one.

The other problem is the notion of an "asynchronous task". What actually happens with async code running on the thread pool is that the thread is returned to the thread pool while the asynchronous operation (i.e., Task.Delay) is in progress. Then, when the async op completes, a thread is taken from the thread pool to resume the async method. In the general case, this is more efficient than reserving a thread specifically to complete that task.

So, with async tasks running on the thread pool, dedicated threads don't really make sense.


Regarding solutions:

If you do need a dedicated thread to run your async code, I'd recommend using the AsyncContextThread from my AsyncEx library:

using (var thread = new AsyncContextThread())
{
  Task t = thread.TaskFactory.Run(async () =>
  {
    while (true)
    {
      cts.Token.ThrowIfCancellationRequested();
      try
      {
        "Running...".Dump();
        await Task.Delay(500, cts.Token);
      }
      catch (TaskCanceledException ex) { }
    }
  });
}

However, you almost certainly don't need a dedicated thread. If your code can execute on the thread pool, then it probably should; and a dedicated thread doesn't make sense for async methods running on the thread pool. More specifically, the long-running flag doesn't make sense for async methods running on the thread pool.

Put another way, with an async lambda, what the thread pool actually executes (and sees as tasks) are just the parts of the lambda in-between the await statements. Since those parts aren't long-running, the long-running flag is not required. And your solution becomes:

Task t = Task.Run(async () =>
{
  while (true)
  {
    cts.Token.ThrowIfCancellationRequested(); // not long-running
    try
    {
      "Running...".Dump(); // not long-running
      await Task.Delay(500, cts.Token); // not executed by the thread pool
    }
    catch (TaskCanceledException ex) { }
  }
});
Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
18

Call Unwrap on the task returned from Task.Factory.StartNew this will return the inner task, which has the correct status.

var cts = new CancellationTokenSource();
Task t = Task.Factory.StartNew(
async () => {
while (true)
{
    cts.Token.ThrowIfCancellationRequested();
    try
    {
        "Running...".Dump();
        await Task.Delay(500, cts.Token);
    }
    catch (TaskCanceledException ex) { }
} }, cts.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default).Unwrap();
NeddySpaghetti
  • 13,187
  • 5
  • 32
  • 61
  • Thank you! This absolutely solves the issue by returning the actual async Task. I will note, though, that people pointed out that having a dedicated thread that has delays is wasteful, and so I will likely be not using a dedicated thread here now. – Ryan Nov 14 '14 at 15:37
6

On a dedicated thread, there's nothing to yield to. Don't use async and await, use synchronous calls.

This question gives two ways to do a cancellable sleep without await:

Task.Delay(500, cts.Token).Wait(); // requires .NET 4.5

cts.WaitHandle.WaitOne(TimeSpan.FromMilliseconds(500)); // valid in .NET 4.0 and later

If part of your work does use parallelism, you can start parallel tasks, saving those into an array, and use Task.WaitAny on the Task[]. Still no use for await in the main thread procedure.

Community
  • 1
  • 1
Ben Voigt
  • 277,958
  • 43
  • 419
  • 720
5

This is unnecessary and Task.Run will suffice as the Task Scheduler will set any task to LongRunning if it runs for more than 0.5 seconds.

See here why. https://blog.stephencleary.com/2013/08/startnew-is-dangerous.html

You need to specify custom TaskCreationOptions. Let’s consider each of the options. AttachedToParent shouldn’t be used in async tasks, so that’s out. DenyChildAttach should always be used with async tasks (hint: if you didn’t already know that, then StartNew isn’t the tool you need). DenyChildAttach is passed by Task.Run. HideScheduler might be useful in some really obscure scheduling scenarios but in general should be avoided for async tasks. That only leaves LongRunning and PreferFairness, which are both optimization hints that should only be specified after application profiling. I often see LongRunning misused in particular. In the vast majority of situations, the threadpool will adjust to any long-running task in 0.5 seconds - without the LongRunning flag. Most likely, you don’t really need it.

rollsch
  • 2,518
  • 4
  • 39
  • 65
5

The real issue you have here is that your operation is not in fact long running. The actual work you're doing is an asynchronous operation, meaning it will return to the caller basically immediately. So not only do you not need to use the long running hint when having the task scheduler schedule it, there's no need to even use a thread pool thread to do this work, because it'll be basically instantaneous. You shouldn't be using StartNew or Run at all, let alone with the long running flag.

So rather than taking your asynchronous method and starting it in another thread, you can just start it right on the current thread by calling the asynchronous method. Offloading the starting of an already asynchronous operation is just creating more work that'll make things slower.

So your code simplifies all the way down to:

var cts = new CancellationTokenSource();
Task t = DoWork();
async Task DoWork()
{
    while (true)
    {
        cts.Token.ThrowIfCancellationRequested();
        try
        {
            "Running...".Dump();
            await Task.Delay(500, cts.Token);
        }
        catch (TaskCanceledException) { }
    }
}
Servy
  • 202,030
  • 26
  • 332
  • 449
2

I think the consideration should be not how long the thread run but how much of its time is it really working. In your example there is short work and them await Task.Delay(...). If this is really the case in your project you probably shouldn't use a dedicated thread for this task and let it run on the regular thread pool. Every time you'll call await on an IO operation or on Task.Delay() you'll release the thread for other tasks to use.

You should only use LongRunning when you'll decrease your thread from the thread-pool and never give it back or give it back only for a small percentage of the time. In such a case (where the work is long and Task.Delay(...) is short in comparison) using a dedicated thread for the job is a reasonable solution. On the other hand if your thread is really working most of the time it will consume system resources (CPU time) and maybe it doesn't really matter if it holds a thread of the thread-pool since it is preventing other work from happening anyway.

Conclusion? Just use Task.Run() (without LongRunning) and use await in your long running task when and if it is possible. Revert to LongRunning only when you actually see the other approach is causing you problems and even then check your code and design to make sure it is really necessary and there isn't something else you can change in your code.

selalerer
  • 3,766
  • 2
  • 23
  • 33