Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Child task canceled, Parent completed?

I'm trying to understand the behavior of the .net Tasks, when children is attached.

I have the following test code:

void Test()
{
    var tokenSource = new CancellationTokenSource();
    var token = tokenSource.Token;

    Task child = null;
    var parent = Task.Factory.StartNew(() =>
    {
        child = Task.Factory.StartNew(() =>
        {
            while (!token.IsCancellationRequested)
                Thread.Sleep(100);
            token.ThrowIfCancellationRequested();
        }, token, TaskCreationOptions.AttachedToParent, TaskScheduler.Default);
    }, token);

    Thread.Sleep(500);

    Debug.WriteLine("State of parent before cancel is {0}", parent.Status);
    Debug.WriteLine("State of child before cancel is {0}", child.Status);

    tokenSource.Cancel();
    Thread.Sleep(500);

    Debug.WriteLine("State of parent is {0}", parent.Status);
    Debug.WriteLine("State of child is {0}", child.Status);
}

Result of this is:

State of parent before cancel is WaitingForChildrenToComplete
State of child before cancel is Running
A first chance exception of type 'System.OperationCanceledException' occurred in mscorlib.dll
State of parent is RanToCompletion
State of child is Canceled

Aparrently the parent task state is not Canceled, even though both tasks share the token, and child is attached.

How do i make the parent task return state Canceled when a cancellation occurs?

NOTE If i throw an exception both tasks return Faulted.

like image 936
Anders Avatar asked Feb 17 '15 14:02

Anders


2 Answers

This is expected behavior as stated on MSDN. The parent task has to wait (scroll down to the cancellation section) for the child task. The parent task has to handle all benign faults (like cancellation).

To make your parent task fail, just wait and pass the token:

Task child = null;
var parent = Task.Factory.StartNew(() =>
{
  child = Task.Factory.StartNew(() =>
  {
    while (!token.IsCancellationRequested) Thread.Sleep(100);
    token.ThrowIfCancellationRequested();
  }, token, TaskCreationOptions.AttachedToParent, TaskScheduler.Default);

  // This is the magic line.
  child.Wait(token);
}, token);

If you are using this code to do something productive and not only for testing, you should also consider using the simplified Task.Run() which supports async delegates instead of Task.Factory.StartNew(). This article is very interesting.

like image 53
Krumelur Avatar answered Oct 27 '22 02:10

Krumelur


Your example is quite convoluted and hides the intuitive behavior and your expectations are wrong.

Let's start from the working example:

void Test()
{
    var tokenSource = new CancellationTokenSource();
    var token = tokenSource.Token;

    Task child = null;
    var parent = Task.Factory.StartNew(() =>
    {
        child = Task.Factory.StartNew(() =>
        {
            while (!token.IsCancellationRequested)
                Thread.Sleep(100);
            token.ThrowIfCancellationRequested();
        }, token, TaskCreationOptions.AttachedToParent, TaskScheduler.Default);

        while (!token.IsCancellationRequested)
            Thread.Sleep(100);
        token.ThrowIfCancellationRequested();
    }, token);

    Thread.Sleep(500);

    Debug.WriteLine("State of parent before cancel is {0}", parent.Status);
    Debug.WriteLine("State of child before cancel is {0}", child.Status);

    tokenSource.Cancel();
    Thread.Sleep(500);

    Debug.WriteLine("State of parent is {0}", parent.Status);
    Debug.WriteLine("State of child is {0}", child.Status);
}

In order for the parent to be canceled, you need to call somewhere in the body of the parent token.ThrowIfCancellationRequested(). However, just calling token.ThrowIfCancellationRequested() won't suffice.

You need to conceptualize how parent and child execution flows go together in order to see why your expectations are wrong.

Main thread: ---\------------------------------------[Cancel]-----/
Parent:          \---\-----[Check cancellation]------------------/
Child:                \------------------------------[Cancel]---/

As you can see from the diagram above, the parent checks for cancellation way before the cancellation is requested. The child receives the cancellation signal because it basically waits for a cancellation to be triggered. Now, if you put the same mechanism in the parent it will receive the cancellation signal because it wouldn't have finished it's job prior to the cancellation being signaled.

like image 43
RePierre Avatar answered Oct 27 '22 01:10

RePierre