28
votes

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?

6
Have you tried StartNew without having an async lambda?Ben Voigt
@BenVoigt You can't await a Task.Delay in a non-async lambda.Asad Saeeduddin
@BenVoigt Yeah, exactly. I need to 'await' on Task.Delay() so that the cancellation request will break the task out of that delay.Ryan
@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
@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));Ben Voigt

6 Answers

28
votes

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) { }
  }
});
15
votes

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();
5
votes

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.

5
votes

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.

4
votes

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) { }
    }
}
2
votes

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.