Computers, Programming, Technology, Music, Literature

Archive for October 2013

TPL – And that’s why it’s called Cooperative Cancellation

leave a comment »

 

This article was originally published for www.prowareness.com and could be located at http://www.prowareness.com/blog/tpl-and-thats-why-its-called-cooperative-cancellation/

 

It’s a practice to provide a Cancel button to a ‘tired of waiting’ user for a long running operation. In WinForms, you would probably create a BackgroundWorker or a separate thread to keep the UI responsive to the user can click the Cancel button. When the user clicks the Cancel button, how you actually implement the cancellation logic is the topic of this blog.

Please read Interrupt Politely, to get a complete overview of the cancellation options. Now, let’s get in to the types introduced with .Net 4.0 and what TPL has to offer.

The CalculateSum method below is our long running operation, that does nothing but adds a couple of numbers; keeps us posted on the calculated value via Console.WriteLine and sleeps on a condition.

        static void CalculateSum(Int64 loopThreshold)
        {
            UInt64 sum = 0;

            for (Int16 i = 0; i < Int16.MaxValue; i++)
            {
                sum += Convert.ToUInt64(i);

                if (i % 100 == 0) //go to pit, take some rest
                {
                    Console.WriteLine(Environment.NewLine + "{0}, Intermediate Sum value {1}", MethodInfo.GetCurrentMethod().Name, sum);
                    Thread.Sleep(1000); //waste time to simulate work
                }
            }

            Console.WriteLine(sum);
        }

 

The method CallerWithoutCancellation calls the above CalculateSum method via a Task.

        static void CallerWithoutCancellation()
        {
            var task = Task.Factory.StartNew(() =>
                {
                    CalculateSum(Int16.MaxValue);
                }
            );
        }

 

Imagine, if the user wanted to cancel this long running task, there is no provision for cancellation that the method CalculateSum provides. If you ran CalculateSum with a classic Thread as opposed to a Task, then you would either issue a Thread.Abort or Thread.Interrupt. This is a decision you have to make carefully. Option 1 in the drdobbs journal and Aborting and Interrupting Threads talk more about them.

But with Task Parallel Library, CancellationTokenSource and CancellationToken, we have better options to cancel.

Let’s look at the implementation of the CalculateSum method with a new overload taking a CancellationToken parameter named inputCancellationToken. Pay careful attention to the line inputCancellationToken.ThrowIfCancellationRequested().

 

        static void CalculateSum(CancellationToken inputCancellationToken, Int16 loopThreshold)
        {
            UInt64 sum = 0;

            for (Int16 i=0; i < loopThreshold; i++)
            {
                sum += Convert.ToUInt64(i);

                if (i % 100 == 0) //go to pit, take some rest
                {
                    Console.WriteLine(Environment.NewLine + "{0}, Intermediate Sum value {1}", MethodInfo.GetCurrentMethod().Name, sum);

                    inputCancellationToken.ThrowIfCancellationRequested(); //accept and honor the cancellation request

                    Thread.Sleep(1000); //waste time to simulate work
                }
            }

            Console.WriteLine(Environment.NewLine + "{0}, Final Sum value {1}", MethodInfo.GetCurrentMethod().Name, sum);
        }

 

The caller, CallerWithCancellationToken, has a cancellation option on press of the Enter key; then it calls cancellationTokenSource.Cancel(). Also pay attention to the second parameter cancellationToken in the Task.Factory.StartNew method.

 

        static void CallerWithCancellation()
        {
            CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
            CancellationToken cancellationToken = cancellationTokenSource.Token;

            var task = Task.Factory.StartNew(() => {
                try
                {
                    CalculateSum2(cancellationToken, Int16.MaxValue);
                }
                catch (Exception ex)
                {
                    Console.Write(Environment.NewLine + ex.GetType().ToString() + Environment.NewLine + ex.Message + Environment.NewLine + ex.StackTrace);
                    throw;
                }
            }, cancellationToken);

            Console.WriteLine(Environment.NewLine + "{0}, ********** Press Enter to cancel the running task **********", MethodInfo.GetCurrentMethod().Name);

            Thread.Sleep(2000); //rest sometime while the task runs

            Console.ReadLine();
            Console.WriteLine(Environment.NewLine + "{0}, ---------- Issuing a cancellation request ----------", MethodInfo.GetCurrentMethod().Name);
            cancellationTokenSource.Cancel(); //issue a cancellation request
            Console.WriteLine(Environment.NewLine + "{0}, ---------- Cancellation request issued ----------", MethodInfo.GetCurrentMethod().Name);
        }

 

The CalculateSum method provides a provision for cancellation using the CancellationToken and the CallerWithCancellation method requests for a cancellation via the same CancellationToken.

As an alternative, you could also poll on the IsCancellationRequested property and proceed with clean up and cancellation.

        static void CalculateSum2(CancellationToken inputCancellationToken, Int16 loopThreshold)
        {
            UInt64 sum = 0;

            for (Int16 i = 0; i < loopThreshold; i++)
            {
                sum += Convert.ToUInt64(i);

                if (i % 100 == 0) 
                {
                    Console.WriteLine(Environment.NewLine + "{0}, Intermediate Sum value {1}", MethodInfo.GetCurrentMethod().Name, sum);

                    if (inputCancellationToken.IsCancellationRequested) //accept and honor the cancellation request
                    {
                        //cleanse if required
                        Console.WriteLine(Environment.NewLine + "{0}, ---------- Cooperating with the cancellation request and exiting ----------", MethodInfo.GetCurrentMethod().Name);
                        return;
                    }

                    Thread.Sleep(1000); //waste time to simulate work
                }
            }

            Console.WriteLine(Environment.NewLine + "{0}, Final Sum value {1}", MethodInfo.GetCurrentMethod().Name, sum);
        }

 

What you effectively have here is a mechanism where the caller signals for a cancellation request via cancellationTokenSource.Cancel(), and the callee responds to it by cancelling itself via inputCancellationToken.ThrowIfCancellationRequested() or performing necessary clean up operations by checking inputCancellationToken.IsCancellationRequested.

 

image

Now, you see a coordination, an agreement between the caller and the callee, or in other words, they both cooperated for cancellation. And that’s why it’s called Cooperative Cancellation.

 

Download and play the source code here.

Note: This pattern existed way before TPL, but you had to rely on synchronization mechanism when reading or writing the cancellation flag. Now with TPL, it is build in with the CancellationTokenSource and CancellationToken.

Written by gmaran23

October 25, 2013 at 7:36 pm