WSC
WSC

Reputation: 992

Why does this cancelled task run to completion

I have a query which takes a long time to return so I shove the function with returns a datatable into a function, within a task like so:

Dim tokenSource As CancellationTokenSource

Private Async Sub btnSomeThing() Handles btnSomething.Click

    tokenSource = New CancellationTokenSource

    Dim cancellationToken As CancellationToken = tokenSource.Token

    Dim t As Task(Of DataTable) = Task.Run(Function()
                                               Return someFunction.GetDataTable()
                                           End Function,
                                           cancellationToken)
    Await t

    Try
        If t.Result.Rows.Count > 0 Then
            ' Display the datatable
        End If
    Catch e As Exception
        If TypeOf e IsNot TaskCanceledException Then
            MessageBox.Show("Error")
        End If
    Finally
        tokenSource.Dispose()
    End Try

End Sub

And I attempt to cancel the task by calling tokenSource.Cancel() elsewhere.

However what I'm seeing is that when Return someFunction.GetDataTable() eventually returns, the code continues to execute as if that task hasn't been cancelled.

I realise that someFunction.GetDataTable() is going to continue until it's done as there's not a nice way to stop a database call once it's started, but when it does return shouldn't task t throw TaskCanceledException and so the rest of the routine is skipped as part of the Try-Catch?

How should I be correctly cancelling t?

EDIT:

Below is the code now. The exception from cancellationToken.ThrowIfCancellationRequested() is not "caught" and so the application just enters breakmode. I have also tried calling cancellationToken.ThrowIfCancellationRequested() within Task.Run, after Await t but all of these have the same effect. Once cancellationToken.ThrowIfCancellationRequested() is called the application crashes.

Dim tokenSource As CancellationTokenSource

Private Async Sub btnSomeThing() Handles btnSomething.Click

    tokenSource = New CancellationTokenSource

    Dim cancellationToken As CancellationToken = tokenSource.Token

    Dim t As Task(Of DataTable) = Task.Run(Function()
                                               Return GetDataTable(cancellationToken)
                                           End Function,
    Try                                       cancellationToken)
        Await t

        If t.Result.Rows.Count > 0 Then
            ' Display the datatable
        End If
    Catch ex As AggregateException
        For Each item In ex.InnerExceptions
            Debug.WriteLine(ex.Message & " " & item.Message)
        Next
    Finally
        tokenSource.Dispose()
    End Try

End Sub

Public Function GetDataTable (cancellationToken As CancellationToken)

    Dim dt As DataTable

    'fill dt from query with DataAdapter

    cancellationToken.ThrowIfCancellationRequested()

    Return dt

End Function

Upvotes: 1

Views: 643

Answers (1)

Torben Schramme
Torben Schramme

Reputation: 2140

There is no way to just kill the task from the outside. That's why there is the concept of a CancellationToken. It's not enough just to pass that token to the task as you are doing in your code. You have to write your own logic inside that task to handle a cancellation request.

Since you are using your Task just to call one deeper function (someFunction.GetDataTable()) you have to implement support for cancellation for this function as well. I.e. pass the cancellation token you got in the task into that function and then do something like this (Pseudocode):

Function GetDataTable(ct As CancellationToken)
    ResultSet = Empty
    While DatabaseTable.HasMoreRecords
        ResultSet.Add DatabaseTable.ReadNext100Records()
        If ct.IsCancellationRequested Then
            ' Do the logic you want to perform when the query should be cancelled
            ' e.g. close db connection or such things

            ' Then call this function to raise the Exception
            ct.ThrowIfCancellationRequested()
        End If
    End While
    Return ResultSet
End Function

So to say in words, create a loop, read only a small subset of rows and check whether cancellation is requested before reading the next records.

For reference have a look here

Edit, to answer your comment here: The reference link gives you a good example how to handle that thrown exception. To summarize, you have to wrap try-catch around awaiting the task:

Try
   Await t
   If t.Result.Rows.Count > 0 Then
       ' Display the datatable
   End If
Catch e As Exception
    If TypeOf e IsNot OperationCanceledException Then
        MessageBox.Show("Error")
    End If
Finally
    tokenSource.Dispose()
End Try

Upvotes: 4

Related Questions