0

If I have an async method:

async Task Process()
{
    while (condition)
    {
        await ...;
    }
}

And I need to schedule the execution of this method. So I use:

Task.Run(Process);

Do I need to keep the reference of Task object returned by Task.Run method in order to ensure that Process executes to completion?


Here is a bit more context: I need to create a lot of (about 50K) task queues in my application. So I want to create a thread-less queue processing design and which has no cost (apart from memory) when queues are empty (as most of these queues will be).

Gist of the sample class: https://gist.github.com/hemant-jangid/e990b27507596c086e5651f504d0521f

Hemant
  • 19,486
  • 24
  • 91
  • 127
  • 1
    First of all why do you use `Task.Run` in the first place. Let framework handle this for you, by using event handlers. Except if you are using console app. Regarding your question, do you care about if it finishes or not? If you don't care about the result, then don't wait for the task. – FCin Aug 08 '18 at 06:05
  • 1
    @FCin not sure what you mean there. `Task` can be used in places where there are _no_ events to be handled –  Aug 08 '18 at 06:06
  • @MickyD It can, but if this is part of operation started by some kind of event then we can quickly solve this problem by leaving it to the framework. It would be good to see how this code is called. – FCin Aug 08 '18 at 06:09
  • Where does `GetData` figure in this? It's not present in your code. *Generally*, `Task.Run` should only be used for *CPU* bound jobs. – Damien_The_Unbeliever Aug 08 '18 at 06:09
  • @Damien_The_Unbeliever Sorry. Fixed the question. I wanted to refer to `Process` function. – Hemant Aug 08 '18 at 06:21
  • Incomplete. What matters most is whether the `...` after `await` are I/O bound or CPU bound. And it is important to know how you call this, from what kind of toplevel code. – bommelding Aug 08 '18 at 06:27
  • @bommelding I don't think it does. Please can you explain why? – Hemant Aug 08 '18 at 06:35

2 Answers2

5

You shouldn't, in general, throw away Tasks. This is not about ensuring that the task will run (it will1), but about observing exceptions. If you don't do something with the Task then the only way to find out that things went wrong is to add an UnobservedTaskException event handler. Not ideal.

Better to have code that awaits it inside a try/catch block or to register a ContinueWith handler where you specify you should be called on faults.

Unfortunately (IMO) the behaviour of .NET changed around 4.5 so that unobserved exceptions no longer tear down the process. Which means that if you fail to observe them, you've got failing code and no means of tracing/logging that situation.


In general, doing Task.Run around a method that already produces a Task is unnecessary and counter-productive. That method has already promised to deliver a Task - why wrap it in a second one? (How and why that method returns the Task is an implementation detail for that method, and none of the business of the caller). You should only really do it if the method that you're calling does significant CPU-bound work and you (the caller) are currently in a "precious" context - such as being on the UI thread.


1Presuming the whole process survives long enough, etc.

Damien_The_Unbeliever
  • 234,701
  • 27
  • 340
  • 448
  • Thanks. I understand the issue of unhandled exceptions (I handle and log them in my real code). The reason I wish to do is that I need to schedule the async queue processing (as shown in linked gist in the question). – Hemant Aug 08 '18 at 06:44
  • @Hemant - I'm not seeing anything there that *requires* `Task.Run`. Assuming `mProcess` is well behaved (either is IO bound and thus returns early with a pending `Task` and/or is CPU bound and so *it* takes care of inserting a `Task.Run` if required), then just do `Run()`. That'll enter the `do` loop, dequeue an item, call `mProcess` and *as soon as that returns an unfinished `Task`*, will return to the caller. Those should all be quite fast ops. – Damien_The_Unbeliever Aug 08 '18 at 06:48
  • If I just call `Run()` in `Add`, then I get a compiler warning saying This call is not awaited. I **don't want** to await. – Hemant Aug 08 '18 at 06:53
  • And does the warning say "wrap this in `Task.Run`" or "you're not observing the results of this `Task` returning method?" (paraphrasing, can't remember the exact wording) – Damien_The_Unbeliever Aug 08 '18 at 06:55
  • 1
    @Hemant - if you want `Run` to be fire and forget and allowed to use `await`, and you're determined that you don't want to observe its results, it should be `async void`, not `async Task`. – Damien_The_Unbeliever Aug 08 '18 at 06:58
  • Yes it makes a lot more sense now. `Task.Run` is indeed not required. I directly called `Run()` and change the `Run()` return type from `Task` to `void`. It is all smooth now. – Hemant Aug 08 '18 at 07:10
0

For this we will have to go deeper into Tasks. Tasks are essentially threads, in its origins Task is scheduled for the thread pool. now consider this quote:

Tasks are essentially threads; but they are background threads. Background threads are automatically aborted when all foreground threads finish. So, if you don't do anything with the task and the program ends, there's a chance the task won't complete.

So you might think, if background threads are automatically aborted when all foreground threads finish, it is essential to keep a reference to task, in fact, this is why you should always wait on tasks as mentioned in this post answer.

However let's drill down into the Thread documentation and look at this line:

It is not necessary to retain a reference to a Thread object once you have started the thread. The thread continues to execute until the thread procedure is complete.

And that means, that although mentioned above that background threads are automatically aborted when all foreground threads finish, there is no need to retain a reference to it.

In fact, according to the documentation, the proper way to create a task is using it's factory:

You can also use the StartNew method to create and start a task in one operation. This is the preferred way to create and start tasks if creation and scheduling do not have to be separated

Barr J
  • 10,636
  • 1
  • 28
  • 46
  • 2
    Regarding last quote. This is probably from .Net 4.0 when there was no `Task.Run`. `Task.Factory.StartNew` is not a good way to start a task in later versions if you do not specify TaskScheduler. `Task.Factory.StartNew` can cause additional issues with unwrapping a task in order to await. Also, in order to achieve the same behaviour with `Task.Factory.StartNew` as with `Task.Run` you would have to call it with many parameters: `Task.Factory.StartNew(() => {}, TaskCreationOptions.DenyChildAttach, default(CancellationToken), TaskScheduler.Default)`. – FCin Aug 08 '18 at 06:26
  • 1
    yes, it is from .net 4.0 :) however I do support the use of TaskCreationOptions in order to use task for long running work (I do not want long running work to be scheduled on my thread pool). – Barr J Aug 08 '18 at 06:27
  • 2
    In most situations, `Task.Run` is the preferred approach. https://blog.stephencleary.com/2013/08/startnew-is-dangerous.html – Hemant Aug 08 '18 at 06:39
  • Where did you get your first quote from? It’s not correct in I/O CP use cases where there is no thread –  Aug 09 '18 at 23:37