4

I have a Task that queries an active directory and populates a list with the results. I have set up my task so that it can be cancelled, however, when the cancel is invoked, the task keeps performing its work. I know the task has been cancelled, as it returns and the actions intended to be performed on task return are run, but the query keeps running in the background, using memory and processing power. The task can be started and "cancelled" repeatedly, with each iteration of the task running and using resources. How can I make the cancel actually cancel?

ViewModel

private async Task RunQuery(QueryType queryType,
    string selectedItemDistinguishedName = null)
{
    StartTask();
    try
    {
        _activeDirectoryQuery = new ActiveDirectoryQuery(queryType,
            CurrentScope, selectedItemDistinguishedName);
        await _activeDirectoryQuery.Execute();
        Data = _activeDirectoryQuery.Data.ToDataTable().AsDataView();
        CurrentQueryType = queryType;
    }
    catch (ArgumentNullException)
    {
        ShowMessage(
            "No results of desired type found in selected context.");
    }
    catch (OutOfMemoryException)
    {
        ShowMessage("The selected query is too large to run.");
    }
    FinishTask();
}

private void CancelCommandExecute()
{
    _activeDirectoryQuery?.Cancel();
}

ActiveDirectoryQuery

public async Task Execute()
{
    _cancellationTokenSource = new CancellationTokenSource();
    var taskCompletionSource = new TaskCompletionSource<object>();
    _cancellationTokenSource.Token.Register(
        () => taskCompletionSource.TrySetCanceled());
    DataPreparer dataPreparer = null;
    var task = Task.Run(() =>
    {
        if (QueryTypeIsOu())
        {
            dataPreparer = SetUpOuDataPreparer();
        }
        else if (QueryTypeIsContextComputer())
        {
            dataPreparer = SetUpComputerDataPreparer();
        }
        else if (QueryTypeIsContextDirectReportOrUser())
        {
            dataPreparer = SetUpDirectReportOrUserDataPreparer();
        }
        else if (QueryTypeIsContextGroup())
        {
            dataPreparer = SetUpGroupDataPreparer();
        }
        Data = GetData(dataPreparer);
    },
    _cancellationTokenSource.Token);
    await Task.WhenAny(task, taskCompletionSource.Task);
}

public void Cancel()
{
    _cancellationTokenSource?.Cancel();
}

Cancel() is called by a Command that is bound to a Button. The task can take several minutes to execute and can consume several hundred megabytes of RAM. If it would help, I can provide any of the referenced methods or any other information.

  • 4
    You need to query the `IsCancellationRequested` property of the token within the task and return from the task accordingly. `Cancel` doesn't just "kill" the task, it just sets the value of that property to true. – KDecker Jul 06 '16 at 15:20
  • @KDecker I'm assuming you mean I need to query it repeatedly? Considering the actual work is done several methods deeper, that is not practical, but my design could be the real problem there. – Michael Brandon Morris Jul 06 '16 at 15:22
  • 1
    @MichaelBrandonMorris yeah you need to test for it. You can also use `ThrowIfCancellationRequested()` Also see [How to: Cancel a Task and Its Children](https://msdn.microsoft.com/en-us/library/dd537607.aspx) – Conrad Frix Jul 06 '16 at 15:24
  • @ConradFrix So the task calls methods (seen above) each of which calls a static method that may call additional static methods. In any case, the brunt of the work is happening at the deepest layer, and the deepest layer needs to return an object. How can I propagate the cancel down the tree while allowing the returned object to propagate up? – Michael Brandon Morris Jul 06 '16 at 15:55
  • 1
    @MichaelBrandonMorris The cancellation token is a variable like any other. The methods for making the variable available to static methods are the same. Typically it means passing the token into each method. – Conrad Frix Jul 06 '16 at 16:00
  • @ConradFrix I realized after posting my last comment how unintelligent it was. I was trying to rewrite all of the methods as async tasks... – Michael Brandon Morris Jul 06 '16 at 16:02
  • As an aside, I would thoroughly appreciate whoever left the downvote to explain why they disliked this question so I may ask better ones in the future. – Michael Brandon Morris Jul 06 '16 at 17:10

1 Answers1

5

Cancellation is cooperative, if you want the actions to cancel you need to edit your functions to cancel. So Execute would become

public async Task Execute()
{
    _cancellationTokenSource = new CancellationTokenSource();
    var taskCompletionSource = new TaskCompletionSource<object>();

    //Token registrations need to be disposed when done.
    using(_cancellationTokenSource.Token.Register(
        () => taskCompletionSource.TrySetCanceled()))
    {
        DataPreparer dataPreparer = null;
        var task = Task.Run(() =>
        {
            if (QueryTypeIsOu())
            {
                dataPreparer = SetUpOuDataPreparer(_cancellationTokenSource.Token);
            }
            else if (QueryTypeIsContextComputer())
            {
                dataPreparer = SetUpComputerDataPreparer(_cancellationTokenSource.Token);
            }
            else if (QueryTypeIsContextDirectReportOrUser())
            {
                dataPreparer = SetUpDirectReportOrUserDataPreparer(_cancellationTokenSource.Token);
            }
            else if (QueryTypeIsContextGroup())
            {
                dataPreparer = SetUpGroupDataPreparer(_cancellationTokenSource.Token);
            }
            Data = GetData(dataPreparer, _cancellationTokenSource.Token);
        },
        _cancellationTokenSource.Token);
        await Task.WhenAny(task, taskCompletionSource.Task);
   }
}

Then from inside those methods. If you have loops in those functions you need to call token.ThrowIfCancellationRequested() from inside the loops. If you don't loop and you call some external API you need to use that API's method of cancellation, hopefully the API will accept a CancellationToken, if it does not and you need to call a .Cancel() method use the Register method like you did in Execute.

If the API does not expose a way to cancel a query the only safe way to stop the query early is you need to move the query to a separate exe. When you do the query you do a var proc = Process.Start(...) to launch the exe. To communicate with it use some form of IPC like WCF over Named Pipes, you can generate a Guid before the process starts and pass it in as a argument then use that guid as the name for the named pipe. If you need to end the query early you do a proc.Kill() to end the external process.

Community
  • 1
  • 1
Scott Chamberlain
  • 124,994
  • 33
  • 282
  • 431
  • Thanks. The code is my own, so I went through and modified every method in there to accept a `CancellationToken`. – Michael Brandon Morris Jul 06 '16 at 17:09
  • That's how I do it, I think it is the only way. It does seem quite messy to have to query the property/pass the token. I've been meaning to ask the question of whether there is a cleaner way, but I doubt it seeing how it works. // This method is also nice because you can cancel "deep" into the calculation instead of waiting for a long running method to complete and cancelling at the "task level". – KDecker Jul 06 '16 at 17:11