Pranay Rana: Abort/Cancel Task

Sunday, December 3, 2017

Abort/Cancel Task

Below post is based on one of the question(Stop hanging synchronous method) I answered on StackOverflow, in which questioner wants to cancel task when its taking too long time to respond i.e. taking too much time in execution and returning result.  But when I tried to provide answer of that question I found there is no direct way to cancel task when its making call outside world i.e. making call to WebService or making call to Database to get data via third party library XenAPI in my case, which is hanging application and not allowing to proceed. To understand this have look to below code.

Var task = Task.Factory.StartNew(()=> CallWebServiceandGetData());

Above line of code creating task which is making call to webservice to get data. Now developer want to write code, such a way that if task take more than 10 second, task get cancel. But in TPL library there is no way to cancel task i.e. there is no direct method or there is no other way to make this task cancel.
So task.Cancel() or task.Abort() like method not exist in TPL. Below post is about how developer can really abort task.

Aborting thread vs Cancelling task

What is really difference between aborting thread and cancelling task.
Aborting thread – In System.Treading , which is library provided for threading prior to TPL library. (Just to note : old System.Threading is still part of.net framework but as TPL provide more control on Task( which is wrapper around thread) provide more control ). In this library to abort thread there is as method called Abort() is available. With help of this method developer can ask execution environment i.e. CLR to abort thread. Below is example code for the same

Thread newThread = new Thread(() => Console.WriteLine("Test"));
newThread.Start();
Thread.Sleep(1000);//sleeping main thread
newThread.Abort();

main thread aborting newly created thread.

Cancelling Task – In newer library TPL (System.Threading.Tasks) , there is no direct method on Task which cancel or abort underlying thread. But there is way to cancel task by using CancellationTokenSource class which allows you to pass CancellationToken as one of the input parameter when you create task. (more on this discussed below). Below is code for cancelling task

var source = new CancellationTokenSource();

CancellationToken token = source.Token;Task.Factory.StartNew(() => { 
 for(int i=0;i< 10000;i++)
 {
   Console.WriteLine(i);
   if (token.IsCancellationRequested)
    token.ThrowIfCancellationRequested();
 }
}, token);
source.CancelAfter(1000);

So above code make use of CancellationTokenSource and CancellationToken provided by it. In above code CancellationTokenSource calls method CancelAfter which set taskCancellation flag. This flag is watched inside delegate via IsCancellationRequested property on CancellationToken. And once it sees in for loop that IsCancellationRequested flag is true it calls ThrowIfCancellationRequested() method and cancels thread.

So in simple term abort thread allows developer to abort executing thread and CancellationToken in new TPL library does same thing, which is called cancelation of task. So basically newer(TPL) and older(Threading) have different way to cancel/abort thread.

But one of the major difference between Abort() thread and Cancel task is Abort() can leave application in inconsistent state ( on Abort()system immediately abort thread not allow to perform any operation to put application in consistent state ),  especially when doing file operation or doing create/update operation , so it better to take care when aborting thread or write code such way that application remain in consistent state. That is one of reason TPL come up with Cancellation mechanism, so those who write code can watch cancellation flag and if it gets true than they can write code to put application in consistence state.

Returning to problem of Cancelling task

By reading above section of “Cancellation Task” one can say there is provision to cancel task which in turn cancel thread also and put system in consistent state, so it’s better approach.  But if we now go to back to scenario where task is got created to fetch data from webService or DataBase which is taking too much long time. So code will be like as below with cancellation mechanism

var source = new CancellationTokenSource();
CancellationToken token = source.Token;
Task.Factory.StartNew(() => { 
    try
    {
        //below is third party library(XenAPI) method 
        HTTP_actions.put_import(…//parameter of method);
        //instead of this there can be database call to get data
        //which takes too much time 
    }
    catch (HTTP.CancelledException exception)
    {
    }
    //execution never comes here till above method get complete           
    if (token.IsCancellationRequested)
        token.ThrowIfCancellationRequested();
              
}, token);
source.CancelAfter(1000);

So in above scenario once call made to API method it never comes back, so control of execution will not return to application and so the code which checks cancellation never get executed till call returns. Which means Task not get cancelled even though after 1000 ms it cancellation flag set to true for cancellation of task.

Above scenario is based on Third party API so it might difficult to understand context. So for easy understanding have look to below code (just one change here , TaskCompletionSource is used to wrap underlying task)

static Task<string> DoWork(CancellationToken token)
        {
            var tcs = new TaskCompletionSource<string>();

            //comment this whole this is just used for testing 
            Task.Factory.StartNew(() =>
            {
                //Simulate work (usually from 3rd party code)
                for (int i = 0; i < 100000; i++)
                    Console.WriteLine("value" + i);

              //execution never comes here till above for loop or        
              //may be long execution /computation get completed           
               if (token.IsCancellationRequested)
                    token.ThrowIfCancellationRequested();

                Console.WriteLine("Task finished!");
            },token);
            tcs.SetResult("Completed");
            return tcs.Task;
        }
       public static void Main()
        {
            var source = new CancellationTokenSource();
            CancellationToken token = source.Token;
            DoWork(token);
             source.CancelAfter(1000);
            Console.ReadLine();
        }

So in above code instead of third party code I replaced it with for loop (or consider long calculation task), now when execution going on application cannot get chance to read cancellation flag which is setup by main thread i.e. from main method. So application cannot able to cancel task till computation get over and control reach to point where cancellation flag check is done. In both of the scenario major problem is when log computation or long call is going on application cannot cancel task, Cancellation mechanism provided in TPL not work and there we need solution to cancel this task other way.

Solution code is
 
    class Program
    {
        //capture request running that , which need to be cancel in case
        // it take more time 
        static Thread threadToCancel = null;
        static async Task<string> DoWork()
        {
            var tcs = new TaskCompletionSource<string>();
            //comment this whole this is just used for testing 
            await Task.Factory.StartNew(() =>
            {
                //Capture the thread
                threadToCancel = Thread.CurrentThread;
                //Simulate work (usually from 3rd party code)
                for (int i = 0; i < 100000; i++)
                     Console.WriteLine("value" + i);
               Console.WriteLine("Task finished!");
            });
            tcs.SetResult("Completed");
            return tcs.Task.Result;
        }

        public static void Main()
        {
            var source = new CancellationTokenSource();
            CancellationToken token = source.Token;
            DoWork();
            //another task check for cancellation flag
            //cancels long running task by calling thread abort method 
            Task.Factory.StartNew(() =>
            {
           //while true below is just for example , in real time 
           //you should replace this with either timer or by thread sleep
           //to save CPU time (or may be better solution other than that) 
           //it better to play around that 
                while (true)
                {
                    if (token.IsCancellationRequested && threadToCancel != null)
                    {
                        threadToCancel.Abort();//abort long running thread
                        Console.WriteLine("Thread aborted");
                        return;
                    }
                }
            });
            //here 1000 can be replace by miliseconds after which you want to 
            // abort thread which calling your long running method 
            source.CancelAfter(1000);
            Console.ReadLine();
        }
    }

Comment in code explains most of the things but let go in detail how it’s going to work. Following are changes in code
  1.  Async/await used to make DoWork method asynchronous
  2.  threadToCancel variable in code stores reference of the Tread of underlying Task by calling Thread.CurrentThread, this variable allows to cancel thread of Task
  3. One more Task got created in main which keep checking Cancellation flag which setup by source.CancelAfter(1000); after 1000 miliseconds 
  4. Task created in Main is running While loop with true, which keep check Cancellation flag true to not, once it gets true this task executes method threadToCancel.Abort(); to abort underlying thread of long running task
So Real magic part in code is reference of underlying thread is stored via Thread.CurrentThread. And separate task runs in main method abort long running task thread when cancellation flag set to true by CancellationSoruce.

No comments:

Post a Comment