3
votes

My problem: I want to use TPL in WinForms application under .NET 4 and I need the task continuations to elevate any unhandled exceptions immediately ("fast throw") instead of waiting for GC collecting the Task. Is it possible?

In .NET 4.5 with async/await support it is possible to write:

Public Class AwaitForm
    Inherits Form

    Private Async Sub Execute()
        Dim uiScheduler = TaskScheduler.FromCurrentSynchronizationContext()

        Try
            Await Me.LongWork().
                ContinueWith(Sub(t) Me.LongWorkCompleted(), uiScheduler)

        Catch ex As Exception
            ' yay, possible to handle here
            ' eg. MsgBox(ex.Message)
            Throw
        End Try
    End Sub

    Private Async Function LongWork() As Task
        Await Task.Delay(1000)
    End Function

    Private Sub LongWorkCompleted()
        Throw New Exception("Ups")
    End Sub

End Class

The exception in continuation would be thrown immediately if not handled in Excecute method.

How to achieve same behavior in .NET 4 without async/await support?

2

2 Answers

1
votes

First of all, you should know it's possible to use async-await with .Net 4.0 with Microsoft.Bcl.Async

But without it, you can add a continuation to the task with ContinueWith and have it run only when there was an exception with TaskContinuationOptions.OnlyOnFaulted

Me.LongWork().ContinueWith(Sub(task) MsgBox(task.Exception.Message), CancellationToken.None, TaskContinuationOptions.OnlyOnFaulted)
1
votes

1) It is either possible to use Microsoft.Bcl.Async as i3arnon suggested.

2) Or if you do not want to reference extra libraries I have came up with the solution based on the async/await. The magic behind is yucky but it's best I've got.

Imports System.Reflection
Imports System.Runtime.CompilerServices
Imports System.Threading


Public Module TaskExtensions

    ''' <summary>Throws the exception on the current SynchronizationContext or ThreadPool if there is none.</summary>
    ''' <param name="task">Task whose faulted continuation should throw exception.</param>
    <Extension()>
    Public Sub ThrowOnFaulted(task As Task)
        Dim context = SynchronizationContext.Current
        ThrowOnFaulted(task, context)
    End Sub


    ''' <summary>Throws the exception on the ThreadPool in given context.</summary>
    ''' <param name="task">Task whose faulted continuation should throw exception.</param>
    ''' <param name="targetContext">The target context on which to propagate the exception. Null to use the ThreadPool.</param>
    <Extension()>
    Public Sub ThrowOnFaulted(task As Task, targetContext As SynchronizationContext)
        task.ContinueWith(Sub(t) ThrowOnFaultedCore(t, targetContext), TaskContinuationOptions.OnlyOnFaulted)
    End Sub


    ''' <remarks>Taken from System.RunTime.CompilerServices.AsyncServices.</remarks>
    Private Sub ThrowOnFaultedCore(task As Task, targetContext As SynchronizationContext)
        Dim exception = task.Exception

        If targetContext IsNot Nothing Then
            Try
                targetContext.Post(Sub(state) Throw DirectCast(state, Exception), exception)
                Return
            Catch ex As Exception
                exception = New AggregateException({exception, ex})
            End Try
        End If

        ThreadPool.QueueUserWorkItem(Sub(state) Throw DirectCast(state, Exception), exception)
    End Sub

End Module

It meets the requirements - exception is thrown "fast" and is can be handled. The exception is Posted to the target SynchronizationContext thus escaping the TPL's exception trapping mechanism. It's far from fast and synchronous but at least it behaves better than waiting for task disposal.