This post is based on one of the questions I answered on StackOverflow, in which the questioner wants to cancel the task when it's taking too long to respond, i.e., taking too much time in execution and returning the result. But, when I tried to provide the answer to that question, I found there is no direct way to cancel the task when making the call to Web Service or making the call to Database to get the data via third-party library ( XenAPI in my case) which is hanging up the application and not allowing it to proceed. To understand this, have a look at the below code.
- Var task = Task.Factory.StartNew(()=> CallWebServiceandGetData());
The above line of code is creating the task which is making calls to the webservice to get the data. Now, the developer wants to write a code in such a way that if the task takes more than 10 seconds, it gets canceled. However, in TPL library, there is no way to cancel the task, i.e., there is no direct method or there is no other way to make this task cancel because task.Cancel() or task.Abort() like methods do not exist in TPL. The below post is about how a developer can really abort a task.
Aborting thread vs Cancelling task
What is the difference between aborting thread and cancelling task.
Aborting thread
System.Treading is a library provided for threading prior to TPL library. (Just to note - old System.Threading is still part of .NET framework but TPL provides more control on Task which is wrapper around thread). In this library, to abort thread there is a method called Abort() available. With the help of this method, a developer can ask execution environment (CLR) to abort the thread. Below is an example code for the same.
- Thread newThread = new Thread(() => Console.WriteLine("Test"));
- newThread.Start();
- Thread.Sleep(1000);
- newThread.Abort();main thread aborting newly created thread.
Cancelling Task
In the newer library, TPL (System.Threading.Tasks), there is no direct method which cancels or aborts the underlying thread. But there is a way to cancel a task by using CancellationTokenSource class which allows you to pass the CancellationToken as one of the input parameters when you create the task. (more on this is discussed below). Below is the code for cancelling the 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, the above code makes use of CancellationTokenSource and CancellationToken provided by it. In the above code, CancellationTokenSource calls the method CancelAfter which sets taskCancellation flag. This flag is watched inside delegate via IsCancellationRequested property on CancellationToken. And once it sees in the for loop that IsCancellationRequested flag is true, it calls the ThrowIfCancellationRequested() method and cancels the thread.
So, in simple terms, abort thread allows the developer to abort executing thread and CancellationToken in new TPL library does the 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 differences between Abort() thread and Cancel task is that Abort() can leave application in an inconsistent state ( on Abort(), the system immediately aborts the thread, not allowing you to perform any operation to put application in consistent state ), especially when doing file operation or doing create/update operation , so it is better to take care when aborting thread or writing code in such a way that the application remains in consistent state. That is one of the reasons TPL come up with Cancellation mechanism, so those who write the code can watch cancellation flag and if it gets true, then they can write the code to put application in consistence state.
Returning to problem of Cancelling task
By reading the above section of “Cancellation Task”, one can say there is a provision to cancel task which in turn cancels the thread also and puts the system in consistent state, so it’s a better approach. But, if we now go back to the scenario where a task is created to fetch the data from webService or Database which is taking too much long time, the code will be like below with cancellation mechanism.
- var source = new CancellationTokenSource();
- CancellationToken token = source.Token;
- Task.Factory.StartNew(() => {
- try
- {
-
- HTTP_actions.put_import(…
-
-
- }
- catch (HTTP.CancelledException exception)
- {
- }
-
- if (token.IsCancellationRequested)
- token.ThrowIfCancellationRequested();
-
- }, token);
- source.CancelAfter(1000);
In the above scenario, once the call is made to API method, it never comes back, so control of execution will not return to the application and the code which checks cancellation never get executed till the call returns. Which means the Task does not get cancelled even after 1000 ms and its cancellation flag is set to true for cancellation of task.
Above scenario is based on Third party API so it might be difficult to understand context. For easy understanding, let us have a look at the below code (just one change here, TaskCompletionSource is used to wrap the underlying task).
- static Task<string> DoWork(CancellationToken token)
- {
- var tcs = new TaskCompletionSource<string>();
-
-
- Task.Factory.StartNew(() =>
- {
-
- for (int i = 0; i < 100000; i++)
- Console.WriteLine("value" + i);
-
-
-
- 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();
- }
In the above code, instead of third-party code, I replaced it with the For loop (or consider long calculation task). Now, when execution is going on, the application cannot get a chance to read the cancellation flag which is set up by main thread i.e. from the main method. So, the application is not able to cancel the task until computation is over and control reaches the point where cancellation flag check is done.
In both of the scenarios, the major problem is when log computation or long call is going on, the application cannot cancel the task. The Cancellation mechanism provided in TPL does not work and there, we need a solution to cancel this task another way.
Solution code is,
- class Program
- {
-
-
- static Thread threadToCancel = null;
- static async Task<string> DoWork()
- {
- var tcs = new TaskCompletionSource<string>();
-
- await Task.Factory.StartNew(() =>
- {
-
- threadToCancel = Thread.CurrentThread;
-
- 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();
-
-
- Task.Factory.StartNew(() =>
- {
- while (true)
- {
- if (token.IsCancellationRequested && threadToCancel != null)
- {
- threadToCancel.Abort();
- Console.WriteLine("Thread aborted");
- return;
- }
- }
- });
-
-
- source.CancelAfter(1000);
- Console.ReadLine();
- }
- }
Comments in the code explain most of the things but let's go into detail about how it’s going to work. Following are the changes in code.
- Async/await used to make DoWork method asynchronous
- threadToCancel variable in code stores the reference of the thread of underlying Task by calling Thread.CurrentThread. This variable allows to cancel the thread of Task.
- One more Task gets created in main which keeps checking the Cancellation flag which is setup by source.CancelAfter(1000); after 1000 miliseconds.
- Task created in Main is running While loop with true, which keeps checking Cancellation flag true or not, once it gets true, this task executes the method threadToCancel.Abort(); to abort the underlying thread of long running task.
So, the real magic part in code is the reference to the underlying thread stored via Thread.CurrentThread. And separate tasks run in the main method to abort long-running task threads when cancellation flag sets to true by CancellationSoruce.