Search code examples
c#.nettask-parallel-librarydispose

Odd Task Parallel Library InvalidOperationException


I have the following code to build an advanced data structure which is pulled from SQL Server, then when the retrevial of that data is complete I update the UI. The code used is

private void BuildSelectedTreeViewSectionAsync(TreeNode selectedNode)
{
    // Initialise.
    SqlServer instance = null;
    SqlServer.Database database = null;

    // Build and expand the TreeNode.
    Task task = null;
    task = Task.Factory.StartNew(() => {
        string[] tmpStrArr = selectedNode.Text.Split(' ');

        string strDatabaseName = tmpStrArr[0];

        instance = SqlServer.Instance(this.conn);

        database = instance.GetDatabaseFromName(strDatabaseName);
    }).ContinueWith(cont => {
        instance.BuildTreeViewForSelectedDatabase(this.customTreeViewSql,
            selectedNode, database);

        selectedNode.Expand();

        task.Dispose();
    }, CancellationToken.None, TaskContinuationOptions.OnlyOnRanToCompletion,
        this.MainUiScheduler);
}

This works as it should on my main development machine; that is, it completes the build of the database object, then in the continuation update the UI and disposes the task (Task object).

However, I have been doing some testing on another machine and I get an InvalidOperationException, this is due to the task.Dispose() on task which still in the Running state, but the continuation cont should never fire unless the task has ran to completion.

Here's what the code looks like in the debugger when the exception is thrown:

enter image description here

I am aware that it almost always unneccessary to call Dispose on tasks. This question is more about why the continuation is firing at all here?**


Solution

  • The reason for this is simple, you are calling Dispose on the continuation itself and not on the first task

    Your code consists of:

    Task task = null;
    var task = <task 1>.ContinueWith(t => { 
        /* task 2 */ 
    
        task.Dispose();
    });
    

    In the above code, task is equal to the continuation (ContinueWith doesn't pass back the original Task, it passes the continuation) and that's what's getting captured in the closure you're passing to ContinueWith.

    You can test this by comparing the references of the Task parameter passed into the ContinueWith method with task:

    Task task = null;
    var task = <task 1>.ContinueWith(t => { 
        /* task 2 */ 
        if (object.ReferenceEquals(t, task))
            throw new InvalidOperationException("Trying to dispose of myself!");
    
        task.Dispose();
    });
    

    In order to dispose of the first, you need to break it up into two Task variables and capture the first Task, like so:

    var task1 = <task 1>;
    var task2 = task1.ContinueWith(t => {
        // Dispose of task1 when done.
        using (task1)
        {
            // Do task 2.
        }
    });
    

    However, because the previous Task is passed to you as a parameter in the ContinueWith method, you don't need to capture task in the closure at all, you can simply call Dispose on the Task passed as a parameter to you:

    var task = <task 1>.ContinueWith(t => {
        // t    = task 1
        // task = task 2
        // Dispose of task 1 when done.
        using (t)
        {
             // Do task 2.
        }
    });