-3

I have a task that is looping reading on a socket:

private async void readLoop() {
    while (!cts.IsCancelRequested()) {
        await socket.ReceiveAsync(data, ...);
        doWork(data);
    }
}

And since the task need not run as soon as it is created, I'm using the Task constructor, rather than Task.Run.

private Task readTask = new Task(readLoop, cts, LongRunning);
// when the task need to run:
readTask.Start()

At this point it works fine, except that when the task need to finish, when I call readTask.Wait() or await readTask, the exceptions happened in ReceiveAsync or doWork are not attached to the readTask. That means even there was exceptions, readTask.Status is RunToComplete and readTask.Exception is null. Accord. to the docs, this is due to the async method returns void, but when I tried this:

private async Task readLoop() {...}

It won't compile:

error CS0407: 'Task WebSocketInitiator.readLoop()' has the wrong return type

So is there anyway to make the task start at a later time and at the same time have it keep track of exceptions happened in the task?

A minimal test case to show the problem: https://dotnetfiddle.net/JIfDIn

fluter
  • 13,238
  • 8
  • 62
  • 100
  • Possible duplicate of https://stackoverflow.com/questions/5383310/catch-an-exception-thrown-by-an-async-method – Peter Duniho Jun 26 '17 at 04:55
  • Possible duplicate of https://stackoverflow.com/questions/17923204/exception-never-reaching-the-handler-in-async-method-c – Peter Duniho Jun 26 '17 at 04:55
  • Possible duplicate of https://stackoverflow.com/questions/41742853/how-to-catch-async-void-method-exception – Peter Duniho Jun 26 '17 at 04:56
  • 3
    Bottom line here: your `async` method returns `void`, which prevents any calling code from observing the exception, because there's no `Task` for the method which can represent the exception. You should not be using `async void` in this context, and had you followed the recommended practice of not doing so, your problem would not happen. Why you get CS0407 when you change the return type I can't say; you must have made a mistake elsewhere as well, but you didn't provide a good [mcve] that reliably reproduces the problem, so what that problem is, no one can say. – Peter Duniho Jun 26 '17 at 04:57
  • @PeterDuniho it won't compile because Task() requires Action as its first arguments, and action is simply void foo(void); It won't accept a Task foo(); – fluter Jun 26 '17 at 05:07
  • So, stop trying to shoehorn a bad design into your code. Again, without a good [mcve] it's impossible to fully understand your scenario. But there's just no reason you need to create the `Task` object prior to starting the task. There are compiler-friendly ways to achieve the same behavior, while still allowing exceptions to be observed. If you cannot bring yourself to use any of those approaches, then just catch the exception in your `readLoop()` method and deliver it by hand to the code that wants it, using whatever mechanism you decide is better than the ones C# already provides you. – Peter Duniho Jun 26 '17 at 05:10
  • So why you need to create that task before running it? Maybe there are better ways to achieve your goal. – Evk Jun 26 '17 at 06:04
  • @PeterDuniho here is a mcve: https://dotnetfiddle.net/JIfDIn although like I said, the problem is just obvious, Task constructor only accept action which is void. - with foo2, no exception is caught, with foo it is ok. – fluter Jun 26 '17 at 07:18
  • MCVEs belong _in the post_. As for your explanation, yes...I understand your description. My previous comment is the information you need, given your clarification. – Peter Duniho Jun 26 '17 at 07:21
  • I'm still not sure why you need to create the Task before you use it. Using the Task constructor is discouraged - it is a legacy since before Task.Run was implemented IIRC. Could you give some more background as to what problem you are trying to solve, which might result in better answers? – default Jun 26 '17 at 07:52
  • I'm trying to manage the task along with the enclosing class, so I'm creating the task in class constructor and dispose it in destructor. But I think I could add another method Start to start the task so that the Task constructor is avoided. – fluter Jun 26 '17 at 07:55
  • Maybe you could be benefited by the `AsyncLazy` implementation? https://blogs.msdn.microsoft.com/pfxteam/2011/01/15/asynclazyt/ – default Jun 26 '17 at 07:58
  • Ok, I will try it, thanks! – fluter Jun 26 '17 at 08:01

2 Answers2

2

If I understand you correctly you want to postpone running the Task until it is needed but you want to keep the creation in a constructor. That is fine, and there is a pattern used for these kind of situations. It's called Lazy.
However, Lazy isn't async. Stephen Taub has however made an async implementation called AsyncLazy.

The reason why you got the error message that you got was because you didn't specify the return value for the outer Task that you created. Also you need to return foos value. If you wrap the call in a lambda which returns the inner Task you can get it though, as such:

var task1 = new Task<Task>(async () => await foo(),
     CancellationToken.None, TaskCreationOptions.LongRunning);
task1.Start();

When you await it however you would await the outer task and not the task that it returns. Thus you need to Unwrap it:

task1.Unwrap().Wait();

This way you would catch the exception. However, this is probably not the best way, since the whole reason to use Task.Run, async and await was to avoid these constructs. In conclusion:

  • Go for AsyncLazy if you need to postpone the calling of your Task
  • Don't call the Task constructor
  • Use Task.Run
default
  • 11,485
  • 9
  • 66
  • 102
2

And since the task need not run as soon as it is created, I'm using the Task constructor, rather than Task.Run.

You should never use the task constructor. It has literally no rational use cases at all.

In your case, it sounds like you don't need a delegate or lazy-initialization, or anything like that; since your members and methods are private, it would make just as much sense to assign the task member at the time it executes:

private async Task readLoop();

// when the task needs to run:
readTask = Task.Run(() => readLoop());

However, if you do need to represent some code to run in the future and not now, then you want a delegate. In this case, an asynchronous delegate: Func<Task>:

Func<Task> func = () => Task.Run(() => readLoop());
...
Task task = func();
Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810