Colin Desmond
Colin Desmond

Reputation: 4854

Exception handling in Parallel.Invoke inside a Task

I have the following code

var exceptions = new ConcurrentQueue<Exception>();
Task task = Task.Factory.StartNew(() =>
{
    try
    {
        Parallel.Invoke(
            async () => await _aViewModel.LoadData(_someId),
            async () => await _bViewModel.LoadData(_someId)
        );
    }
    catch (Exception ex)
    {
        exceptions.Enqueue(ex);
    }
}).ContinueWith((continuation) =>
    {
        if (exceptions.Count > 0) throw new AggregateException(exceptions);
    });

I am using Task.StartNew here because the LoadData method use the Dispatcher.StartAsync method to invoke on the main UI thread internally.

The problem I have is that if I force _aViewModel.LoadData to throw an exception it is not caught in the Catch(Exception) clause (nor if I catch AggregateException). I don't understand why!?

Upvotes: 2

Views: 1632

Answers (2)

Stephen Cleary
Stephen Cleary

Reputation: 456507

Parallel.Invoke is not async-aware. So your async lambdas are being converted to async void methods, which have extremely awkward error semantics (they are not allowed to leave the async void method; instead, they are captured and re-raised directly on the SynchronizationContext that was active at the time the async void method started - in this case, the thread pool).

I'm not sure why you have the Parallel.Invoke in the first place. Since your method is already async, you could just do something like this:

Task task = Task.Factory.StartNew(async () =>
{
    try
    {
        Task.WaitAll(
            _aViewModel.LoadData(_someId),
            _bViewModel.LoadData(_someId)
        );
    }
    catch (Exception ex)
    {
        exceptions.Enqueue(ex);
    }
})...

P.S. If you have the time, rethink the structure of this whole part of the code. Dispatcher.StartAsync is a code smell. The UI should be (asynchronously) requesting data; the data retrieval objects should not have to know about the UI.

Upvotes: 5

Richard Deeming
Richard Deeming

Reputation: 31198

Parallel.Invoke takes an array of Action delegates. It has no means of knowing that your delegates are actually async methods, and therefore it returns before your tasks have completed.

For an in-depth explanation of this behaviour, watch Lucian Wischik's Channel 9 video on the subject.

Try changing your code to use the Task.WhenAll method instead.

var aTask = _aViewModel.LoadData(_someId);
var bTask = _bViewModel.LoadData(_someId);
await Task.WhenAll(aTask, bTask);

Upvotes: 3

Related Questions