2

I have two async methods that I am running in the background of a form window as separate threads/tasks. These are infinite loops that just do some work in the background and then update the UI using the dispatcher. See below.

    public async Task RunCameraThread(CancellationToken cancelToken)
    {
        while (true)
        {
            // If cancellation token is set, get out of the thread & throw a cancel exception
            cancelToken.ThrowIfCancellationRequested();

            // Get an image from the camera
            CameraBitmap = Camera.CaptureImage(true);

            // Update the UI (use lock to prevent simultaneous use of Dispatcher object in other thread)
            lock (Dispatcher)
            {
                Dispatcher.Invoke(() => pictureBoxCamera.Image = tempBitmap);
                Dispatcher.Invoke(() => pictureBoxCamera.Invalidate());
            }
        }
    }

    public async Task RunDistanceSensorThread(CancellationToken cancelToken)
    {
        while (true)
        {
            // If cancellation token is set, get out of the thread & throw a cancel exception
            cancelToken.ThrowIfCancellationRequested();

            // Get the distance value from the distance sensor
            float distance = Arduino.AverageDistance(10, 100);

            // Update the UI (use lock to prevent simultaneous use of Dispatcher object)
            lock (Dispatcher)
            {
                Dispatcher.Invoke(() => textBoxDistanceSensor.Text = distance.ToString("0.00"));
            }
        }
    }

These tasks are started on a button click (code shown below). I'm trying to use await Task.WhenAll in order to await both tasks. When the cancellation token is set this works as intended and an OperationCanceledException is caught. However, any exceptions thrown by issues with the Camera or Arduino (simulated by simply unplugging the USB during a run), does not seem to be caught.

    private async void buttonConnect_Click(object sender, EventArgs e)
    {
        try
        {
            // Disable UI so we cannot click other buttons
            DisableComponentsUI();
            // Connect to Nimbus, Camera and Arduino
            await Task.Run(() => Nimbus.ConnectAsync());
            Camera.Connect();
            Camera.ManagedCam.StartCapture();
            Arduino.Connect();
            // Get the current Nimbus positions and enable UI
            UpdatePositionsUI();
            EnableComponentsUI();
            // Reset cancel token and start the background threads and await on them (this allows exceptions to bubble up to this try/catch statement)
            StopTokenSource = new CancellationTokenSource();
            var task1 = Task.Run(() => RunCameraThread(StopTokenSource.Token));
            var task2 = Task.Run(() => RunDistanceSensorThread(StopTokenSource.Token));
            await Task.WhenAll(task1, task2);
        }
        catch (OperationCanceledException exceptionMsg)
        {
            // Nothing needed here...
        }
        catch (Hamilton.Components.TransportLayer.ObjectInterfaceCommunication.ComLinkException exceptionMsg)
        {
            NimbusExceptionHandler(exceptionMsg);
        }
        catch (FlyCapture2Managed.FC2Exception exceptionMsg)
        {
            CameraExceptionHandler(exceptionMsg);
        }
        catch (IOException exceptionMsg)
        {
            ArduinoExceptionHandler(exceptionMsg);
        }
        catch (UnauthorizedAccessException exceptionMsg)
        {
            ArduinoExceptionHandler(exceptionMsg);
        }
        catch (TimeoutException exceptionMsg)
        {
            ArduinoExceptionHandler(exceptionMsg);
        }
}

What's strange is that I see the exceptions thrown in the output window, but they don't bubble up to my try/catch. Also, if I simply await on one task it works as expected and the exception bubbles up.

Anyone have any idea what I'm doing wrong?

Thanks!

Uwe Keim
  • 39,551
  • 56
  • 175
  • 291
leonhart88
  • 125
  • 2
  • 8
  • @StenPetrov yes I was expecting that. It does seem to throw an exception as I can see it in the output window, but it doesn't bubble up to my try/catch when using .WhenAll – leonhart88 Mar 15 '17 at 18:24
  • 1
    @StenPetrov `WhenAll` will throw an `AggregateException` that contains the exceptions form every single exception from every task that faulted, not just the first. – Servy Mar 15 '17 at 18:40
  • @servy are you sure? I think you have to call `Wait` on the `WhenAll` task if you want to catch exceptions thrown by the individual tasks as shown [here](https://msdn.microsoft.com/en-us/library/hh194874(v=vs.110).aspx). I dont' know the relation to `await` ing the WhenAll task, however. – Quantic Mar 15 '17 at 18:55
  • @Quantic Yes, I'm quite sure that `WhenAll` does in fact represent the exceptions of every faulted task. That it's what you see when you call `Wait` of course demonstrates this. – Servy Mar 15 '17 at 18:57
  • @Servy I mean the only calls that are ever wrapped in a `try` are `Wait` and `WaitAll` as seen at [Exception Handling Task Parallel Library](https://msdn.microsoft.com/en-us/library/dd997415(v=vs.110).aspx): "Exceptions are propagated when you use one of the static or instance Task.Wait or Task.Wait methods", no where do they wrap `WhenAll` and even the [`WhenAll`](https://msdn.microsoft.com/en-us/library/hh194874(v=vs.110).aspx) page shows no try around WhenAll, only around Wait. But again I don't know if there is different behavior when using `await`. – Quantic Mar 15 '17 at 19:02
  • @Quantic The call to `WhenAll` doesn't itself throw, it returns a task that is faulted and whose exception information contains all of the exceptions from all of the faulted tasks. If you wait on that task, then it would re-throw that aggregate exception. – Servy Mar 15 '17 at 19:05
  • 1
    @servy Thanks for clarifying, I did find the same syntax of `await Task.WhenAll` in the last example [here](https://msdn.microsoft.com/en-us/library/0yd65esw.aspx), however a caveat that may be important for this question is: "the task might be the result of a call to Task.WhenAll. When you await such a task, only one of the exceptions is caught, and you can't predict which exception will be caught.". – Quantic Mar 15 '17 at 19:13
  • @Quantic That's the behavior of `await`, not the behavior of `WhenAll`. `WhenAll` is providing an exception with all of the exceptions in it, `await` is picking one of them. – Servy Mar 15 '17 at 19:15
  • 1
    Take a look here, it may help https://codeblog.jonskeet.uk/2010/11/04/multiple-exceptions-yet-again-this-time-with-a-resolution/ – Sten Petrov Mar 15 '17 at 19:16
  • @StenPetrov That book is wrong, as you can trivially see by using their own program and simply looking at the `Exception` property of the task returned by `WhenAll` yourself. Note that just because someone wrote it in a book doesn't mean it's right. – Servy Mar 15 '17 at 19:20
  • Are we looking at the complete code? I noticed the method signature is like `public async Task RunDistanceSensorThread(CancellationToken cancelToken)` but there is no `Task` awaited in the method. If you somehow do an `await` in the code then why are you wrapping it in a call to `Task.Run()`? Or, if no Tasks are called in the method change the signature to 'return' void instead of a Task to minimize confusion. – Peter Bons Mar 15 '17 at 19:36
  • @PeterBons Yes that's the code. I'm not doing any awaits in the code other than the await in the buttonConnectClick handler. I suppose I could change the return but I'm hoping to solve the issue of exceptions first. – leonhart88 Mar 15 '17 at 22:53
  • Even if await picks only one of the exceptions, I still expect the exception to bubble up to the top. Again, I'm seeing the exception in the Visual Studio output window, but it doesn't bubble up to the try/catch in the buttonConnectClick handler. – leonhart88 Mar 15 '17 at 22:55
  • 1
    @leonhart88: The problem isn't obvious from the code you posted. Try [reducing to a minimal, complete example](https://stackoverflow.com/help/mcve), and you'll probably discover the solution yourself. – Stephen Cleary Mar 16 '17 at 12:59
  • @Servy *That's the behavior of `await`, not the behavior of `WhenAll`. `WhenAll` is providing an exception with all of the exceptions in it, `await` is picking one of them.* AFAIK, it is not behavior of `await` but behavior of `awaiter.GetResult()`. You can have same Exception propagation behavior by calling `task.GetAwaiter().GetResult()`. – user4003407 Mar 16 '17 at 14:51

1 Answers1

10

This line

await Task.WhenAll(task1, task2);

will throw AggregateException if it occurs in task1 and / or task2, and will contain exceptions from all the tasks inside.

BUT for this to occur (i.e. for you to receive AggregateException) all tasks should finish their execution.

So in your current state you will receive exception only when exceptions occurred in both tasks (sooner or later).

If you do need to stop all other tasks whenever one of them failed, you can try using for example Task.WhenAny instead of Task.WhenAll.

Another option would be to implement some manual synchronization - for example, introduce shared flag like "wasAnyExceptions", set it inside every task whenever exception in that task occur, and check it inside task loop to stop loop execution.

UPDATE based on comments

To clarify, Task.WhenAll(..) will return task. When this task is finished, it will contain AggregateException with exceptions from all failed tasks inside its Exception property.

If you await for such task it will throw unwrapped exception from the first faulted task in the list.

If you .Wait() for this task, you will receive AggregateException.

Lanorkin
  • 7,310
  • 2
  • 42
  • 60
  • 1
    Thanks @Lanorkin, your comment helped me find the answer. I can confirm Task.WhenAll keeps waiting for the other task to finish. Task.WhenAny is what I ended up using, as it will force completion when any of the tasks throw an exception. What I did notice was that when one task had an exception, Task.WhenAny would still not throw the exception up, it just returned a completed task. In order to bubble the exceptions up, you need to await the returned task from Task.WhenAny. See http://stackoverflow.com/questions/31544684/why-does-the-task-whenany-not-throw-an-expected-timeoutexception – leonhart88 Mar 16 '17 at 18:25
  • 1
    @leonhart88 I think you have easier way actually - just take a look at task status after WaitAny & observe task.Exception property https://msdn.microsoft.com/en-us/library/system.threading.tasks.task.exception(v=vs.110).aspx Also keep in mind that both tasks could fail, but WaitAny will return only the first one, so you might want to analyze the other tasks too. I in similar situation used manual synchronization as it was more transparent to review & use. – Lanorkin Mar 17 '17 at 05:16
  • @Lanorkin, small correction: `await Task.WhenAll(task1, task2)` will *not* throw an AggregateException. `await` will unwrap that exception and instead throw the exception of the first task that failed. The task returned by `Task.WhenAll` will be the one that contains the `AggregateException`. – joerage Mar 30 '17 at 02:09
  • @joerage yes, exactly – Lanorkin Mar 30 '17 at 05:22
  • @joerage and just to clarify *first* meaning not first in time occurrence, but first failed task in the order you used for `WhenAll` list; so if both tasks in `Task.WhenAll(task1, task2)` failed, no matter which one failed first *in time*, `await` will always unwrap exception from `task1` – Lanorkin Mar 30 '17 at 05:29
  • Related: https://stackoverflow.com/a/62607500/1768303 – noseratio Jun 27 '20 at 08:49