cancelling a function - c#

i have a function that makes a few http requests, and runs in a task.
that function can be interrupted in the middle, since i cant abort the task i added some boolean conditions in the function.
example:
public int foo(ref bool cancel)
{
if(cancel)
{
return null
}
//do some work...
if(cancel)
{
return null
}
//http webrequest
if(cancel)
{
return null
}
}
thisworked pretty good, although this is quite some ugly code.
another problem is when i already executed the web request, and it takes time for me to get the response than the function cncelation takes a lot of time (till i get a response).
is there a better way for me to check this? or mybe i should use threads instead of task?
edit
i added a cancelation token: declared a cancelationTokenSource, and passed its token to the task
CancellationTokenSource cncelToken = new CancellationTokenSource();
Task t = new Task(() => {foo()},cancelToken.token);
when i do cancelToken.Cancel();
i still wait for the response, and the tsk isnt cancelling.

Tasks support cancellation - see here.
Here's a quick snippet.
class Program
{
static void Main(string[] args)
{
var token = new CancellationTokenSource();
var t = Task.Factory.StartNew(
o =>
{
while (true)
Console.WriteLine("{0}: Processing", DateTime.Now);
}, token);
token.CancelAfter(1000);
t.Wait(token.Token);
}
}
Remember to wait for the task using the provided cancellation token. You ought to receive an OperationCanceledException.

Related

Using cancellation token properly in c#

I was recently exposed to C# language and was working on getting data out of cassandra so I was working with below code which gets data from Cassandra and it works fine.
Only problem I have is in my ProcessCassQuery method - I am passing CancellationToken.None to my requestExecuter Function which might not be the right thing to do. What should be the right way to handle that case and what should I do to handle it correctly?
/**
*
* Below method does multiple async calls on each table for their corresponding id's by limiting it down using Semaphore.
*
*/
private async Task<List<T>> ProcessCassQueries<T>(IList<int> ids, Func<CancellationToken, int, Task<T>> mapperFunc, string msg) where T : class
{
var tasks = ids.Select(async id =>
{
await semaphore.WaitAsync();
try
{
ProcessCassQuery(ct => mapperFunc(ct, id), msg);
}
finally
{
semaphore.Release();
}
});
return (await Task.WhenAll(tasks)).Where(e => e != null).ToList();
}
// this might not be good idea to do it. how can I improve below method?
private Task<T> ProcessCassQuery<T>(Func<CancellationToken, Task<T>> requestExecuter, string msg) where T : class
{
return requestExecuter(CancellationToken.None);
}
As said in the official documentation, the cancellation token allows propagating a cancellation signal. This can be useful for example, to cancel long-running operations that for some reason do not make sense anymore or that are simply taking too long.
The CancelationTokenSource will allow you to get a custom token that you can pass to the requestExecutor. It will also provide the means for cancelling a running Task.
private CancellationTokenSource cts = new CancellationTokenSource();
// ...
private Task<T> ProcessCassQuery<T>(Func<CancellationToken, Task<T>> requestExecuter, string msg) where T : class
{
return requestExecuter(cts.Token);
}
Example
Let's take a look at a different minimal/dummy example so we can look at the inside of it.
Consider the following method, GetSomethingAsync that will yield return an incrementing integer every second.
The call to token.ThrowIfCancellationRequested will make sure a TaskCanceledException is thrown if this process is cancelled by an outside action. Other approaches can be taken, for example, check if token.IsCancellationRequested is true and do something about it.
private static async IAsyncEnumerable<int> GetSomethingAsync(CancellationToken token)
{
Console.WriteLine("starting to get something");
token.ThrowIfCancellationRequested();
for (var i = 0; i < 100; i++)
{
await Task.Delay(1000, token);
yield return i;
}
Console.WriteLine("finished getting something");
}
Now let's build the main method to call the above method.
public static async Task Main()
{
var cts = new CancellationTokenSource();
// cancel it after 3 seconds, just for demo purposes
cts.CancelAfter(3000);
// or: Task.Delay(3000).ContinueWith(_ => { cts.Cancel(); });
await foreach (var i in GetSomethingAsync(cts.Token))
{
Console.WriteLine(i);
}
}
If we run this, we will get an output that should look like:
starting to get something
0
1
Unhandled exception. System.Threading.Tasks.TaskCanceledException: A task was canceled.
Of course, this is just a dummy example, the cancellation could be triggered by a user action, or some event that happens, it does not have to be a timer.

How to call async method in a thread but wait for it in c#

I have a thread which is responsible for calling a webapi from 4 websites exactly every 2 seconds. The Webapi call method should not be awaited because if a website is not available it will wait 5 second to get timeout and then the next website call will be delayed.
As HttpClient in .NET 4.7.2 has only async methods , it should be used with await, and if not , compiler gives warning and we may get unexpected behavior (as Microsoft says) .
So should I use Task.Run or call Threadpool.QueueUserWorkItem to make a webapi call in parallel.
Here is sudocode :
public class Test1
{
private AutoResetEvent waitEvent = new AutoResetEvent(false);
private volatile bool _terminated = false;
public void Start()
{
Thread T = new Thread(ProcThread);
T.Start();
}
private async void ProcThread()
{
while (!_terminated)
{
await CallWebApi(); <=========== this line
waitEvent.WaitOne(2000);
}
}
private async Task CallWebApi()
{
HttpClient client = new HttpClient();
.....
.....
}
}
So you have an async procedure that uses a HttpClient to fetch some information and process the fetched data:
async Task CallWebApiAsync() {...}
Improvement 1: it is good practice to suffix async methods with async. This is done to make it possible to let an async version exist next to a non-async version that does something similarly.
Inside this method you are using one of the HttpClient methods to fetch the information. As CallWebApiAsync is awaitable, I assume the async methods are used (GetAsync, GetStreamAsync, etc), and that the method only awaits when it needs the result of the async method.
The nice thing about this is, that as a user of CallWebApiAsync, as long as you don't await the call, you are free to do other things, even if the website isn't reacting. The problem is: after 2 seconds, you want to call the method again. But what to do if the method hasn't finished yet.
Improvement 2 Because you want to be able to start a new Task, while the previous one has not finished: remember the started tasks, and throw them away when finished.
HashSet<Task> activeTasks = new HashSet<Task>(); // efficient add, lookup, and removal
void TaskStarted(Task startedTask)
{
// remember the startedTask
activeTasks.Add(startedTask);
}
void TaskCompleted(Task completedTask)
{
// If desired: log or process the results
LogFinishedTask(completedTask);
// Remove the completedTask from the set of ActiveTasks:
activeTasks.Remove(completedTask);
}
It might be handy to remove all completed tasks at once:
void RemoveCompletedTasks()
{
var completedTasks = activeTasks.Where(task => task.IsCompleted).ToList();
foreach (var task in completedTasks)
{
TaskCompleted(completedTask);
}
}
Now we can adjust your ProcThread.
Improvement 3: in async-await always return Task instead of void and Task<TResult> instead of TResult. Only exception: eventhandlers return void.
async Task ProcThread()
{
// Repeatedly: start a task; remember it, and wait 2 seconds
TimeSpan waitTime = TimeSpan.FromSeconds(2);
while (!terminationRequested)
{
Task taskWebApi = CallWebApiAsync();
// You didn't await, so you are free to do other things
// Remember the task that you started.
this.TaskStarted(taskWebApi);
// wait a while before you start new task:
await Task.Delay(waitTime);
// before starting a new task, remove all completed tasks
this.RemoveCompletedTasks();
}
}
Improvement 4: Use TimeSpan.
TimeSpan.FromSeconds(2) is much easier to understand what it represents than a value 2000.
How to stop?
The problem is of course, after you request termination there might still be some tasks running. You'll have to wait for them to finish. But even then: some tasks might not finish at all within reasonable time.
Improvement 5: use CancellationToken to request cancellation.
To cancel tasks in a neat way, class CancellationToken is invented. Users who start a task create a CancellationTokenSource object, and ask this object for a CancellationToken. This token is passed to all async methods. As soon as the user wants to cancel all tasks that were started using this CancellationTokenSource, he requests the CancellationTokenSource to cancel.
All tasks that have a token from this source have promised to regularly check the token to see if cancellation is requested. If so, the task does some cleanup (if needed) and returns.
Everything summarized in one class:
class Test1
{
private HttpClient httpClient = new HttpClient(...);
private HashSet<TTask> activeTasks = new HashSet<TTask>();
public async Task StartAsync(CancellationToken cancellationToken)
{
// repeated CallWebApiAsync until cancellation is requested
TimeSpan waitTime = TimeSpan.FromSeconds(2);
// repeat the following until OperationCancelled
try
{
while (true))
{
// stop if cancellation requested
cancellationToken.ThrowIfCancellationRequested();
var taskWebApi = this.CallWebApiAsync(cancellationToken);
this.activeTasks.Add(taskWebApi);
await Task.Delay(waitTime, cancellationToken);
// remove all completed tasks:
activeTasks.RemoveWhere(task => task.IsCompleted);
}
}
catch (OperationCanceledException exception)
{
// caller requested to cancel. Wait until all tasks are finished.
await Task.WhenAll(this.activeTasks);
// if desired do some logging for all tasks that were not completed.
}
}
And the adjusted CallWebApiAsync:
private async Task CallWebApiAsync(CancellationToken cancellationToken)
{
const string requestUri = ...
var httpResponseMessage = await this.httpClient.GetAsync(requestUri, cancellationToken);
// if here: cancellation not requested
this.ProcessHttpResponse(httpResponseMessage);
}
private void ProcessHttpRespons(HttpResponseMessage httpResponseMessage)
{
...
}
}
Usage:
CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
Test1 test = new Test1();
Task taskCallWebApiRepeatedly = test.StartAsync(cancellationTokenSource.Token);
// because you didn't await, you are free to do other things, while WebApi is called
// every 2 seconds
DoSomethingElse();
// you get bored. Request cancellation:
cancellationTokenSource.Cancel();
// of course you need to await until all tasks are finished:
await Task.Wait(taskCallWebApiRepeatedly);
Because everyone promises to check regularly if cancellation is requested, you are certain that within reasonable time all tasks are finished, and have cleaned up their mess. The definition or "reasonable time" is arbitrary, but let's say, less than 100 msec?
If all you want is to execute a method every two seconds, then a System.Timers.Timer is probably the most suitable tool to use:
public class Test1
{
private readonly HttpClient _client;
private readonly System.Timers.Timer _timer;
public Test1()
{
_client = new HttpClient();
_timer = new System.Timers.Timer();
_timer.Interval = 2000;
_timer.Elapsed += Timer_Elapsed;
}
private void Timer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{
var fireAndForgetTask = CallWebApiAsync();
}
private async Task CallWebApiAsync()
{
var html = await _client.GetStringAsync("http://example.com");
//...
}
public void Start() => _timer.Start();
public void Stop() => _timer.Stop();
}
something like this. BTW take this as pseudo code as I am typing sitting on my bed:)
List<Task> tasks = new List<Task>();
tasks.Add(CallWebApi());
while (! await Task.WhenAny(tasks))
{
tasks.Add(CallWebApi()); <=========== this line
await Task.Delay(2000);
}

C# Xamarin Forms - Executing task, with timeout

Like many others, I need to write a function that returns a task, and I want that task to automatically time out after a certain period.
The initial code looks like this:
class MyClass
{
TaskCompletionSource<string> m_source;
public Task<string> GetDataFromServer()
{
m_source = new TaskCompletionSource<string> ();
// System call I have no visibility into, and that doesn't inherently take any
// sort of timeout or cancellation token
ask_server_for_data_and_when_youve_got_it_call(Callback);
return m_source.Task;
}
protected void Callback(string data);
{
// Got the data!
m_source.TrySetResult(data);
}
}
Now I want this to be a little smarter, and time itself out when appropriate. I have several options for doing this:
class MyClass
{
TaskCompletionSource<string> m_source;
public Task<string> GetDataFromServer(int timeoutInSeconds)
{
m_source = new TaskCompletionSource<string> ();
ask_server_for_data_and_when_youve_got_it_call(Callback);
// Method #1 to set up the timeout:
CancellationToken ct = new CancellationToken ();
CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource (ct);
cts.CancelAfter (timeoutInSeconds * 1000);
cts.Token.Register(() => m_source.TrySetCancelled());
// Method #2 to set up the timeout:
CancellationTokenSource ct2 = new CancellationTokenSource ();
ct2.CancelAfter (timeoutInSeconds * 1000);
ct2.Token.Register (() => m_source.TrySetCancelled());
// Method #3 to set up the timeout:
System.Threading.Tasks.Task.Factory.StartNew (async () =>
{
await System.Threading.Tasks.Task.Delay (timeoutInSeconds * 1000);
m_source.TrySetCancelled();
});
// Method #4 to set up the timeout:
Xamarin.Forms.Device.StartTimer (new TimeSpan (0, 0, timeoutInSeconds),
() => m_source.TrySetCancelled());
return m_source.Task;
}
protected void Callback(string data);
{
// Got the data!
m_source.TrySetResult(data);
}
}
What are the plusses and minuses to the 4 different ways of setting up a timeout? For instance, I'm guessing that method #2 is the most "lightweight" (requiring the fewest system resources)?
Are there other ways to set up a timeout that I've missed?
p.s.
One piece of knowledge I found out the hard way - if you call GetDataFromServer() from a thread besides the main UI thread:
Task.Run(() => await GetDataFromServer());
On iOS, the fourth method (Xamarin.Forms.Device.StartTimer) never fires
I think it's easier to just use Task.Delay and Task.WhenAny:
public async Task<string> GetDataFromServerAsync(int timeoutInSeconds)
{
Task<string> requestTask = GetDataFromServerAsync();
var timeoutTask = Task.Delay(timeoutInSeconds);
var completedTask = await Task.WhenAny(requestTask, timeoutTask);
if (completedTask == timeoutTask)
throw new OperationCanceledException();
return await requestTask;
}
Cons of the other approaches:
Method #1: Creates a new CancellationToken for no reason. It's just a less-efficient version of Method #2.
Method #2: Normally, you should be disposing the result of Register once the task completes. In this case, it would probably work OK since the CTS is always eventually cancelled.
Method #3: Uses StartNew just to call Delay - not sure of the reasoning there. It's essentially a less-efficient version of Delay with WhenAny.
Method #4: Would be acceptable. Though you do have to deal with TaskCompletionSource<T> and its quirks (e.g., synchronous continuations by default).

C# Task Ignoring Cancellation timeout

I'm trying to write a wrapper for arbitrary code that will cancel (or at least stop waiting for) the code after a given timeout period.
I have the following test and implementation
[Test]
public void Policy_TimeoutExpires_DoStuff_TaskShouldNotContinue()
{
var cts = new CancellationTokenSource();
var fakeService = new Mock<IFakeService>();
IExecutionPolicy policy = new TimeoutPolicy(new ExecutionTimeout(20), new DefaultExecutionPolicy());
Assert.Throws<TimeoutException>(async () => await policy.ExecuteAsync(() => DoStuff(3000, fakeService.Object), cts.Token));
fakeService.Verify(f=>f.DoStuff(),Times.Never);
}
and the "DoStuff" method
private static async Task DoStuff(int sleepTime, IFakeService fakeService)
{
await Task.Delay(sleepTime).ConfigureAwait(false);
var result = await Task.FromResult("bob");
var test = result + "test";
fakeService.DoStuff();
}
And the implementation of IExecutionPolicy.ExecuteAsync
public async Task ExecuteAsync(Action action, CancellationToken token)
{
var cts = new CancellationTokenSource();//TODO: resolve ignoring the token we were given!
var task = _decoratedPolicy.ExecuteAsync(action, cts.Token);
cts.CancelAfter(_timeout);
try
{
await task.ConfigureAwait(false);
}
catch(OperationCanceledException err)
{
throw new TimeoutException("The task did not complete within the TimeoutExecutionPolicy window of" + _timeout + "ms", err);
}
}
What should happen is that that the test method attempts to take >3000ms and the timeout should occur at 20ms, but this is not happening. Why does my code not timeout as expected?
EDIT:
As requested - the decoratedPolicy is as follows
public async Task ExecuteAsync(Action action, CancellationToken token)
{
token.ThrowIfCancellationRequested();
await Task.Factory.StartNew(action.Invoke, token);
}
If I understand correctly, you're trying to support timeout for a method which doesn't supports timeout / cancellation.
Typically this is done with a starting a timer with required timeout value. If the timer fires first, then you can throw exception. With TPL, you can use Task.Delay(_timeout) instead of a timer.
public async Task ExecuteAsync(Action action, CancellationToken token)
{
var task = _decoratedPolicy.ExecuteAsync(action, token);
var completed = await Task.WhenAny(task, Task.Delay(_timeout));
if (completed != task)
{
throw new TimeoutException("The task did not complete within the TimeoutExecutionPolicy window of" + _timeout + "ms");
}
}
Note: This doesn't stops the execution of _decoratedPolicy.ExecuteAsync method rather it ignores it.
If your method do support cancellation(but not in a timely manner) then it is better to cancel the Task after the timeout. You can do it by creating a Linked Token.
public async Task ExecuteAsync(Action action, CancellationToken token)
{
using(var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(token))
{
var task = _decoratedPolicy.ExecuteAsync(action, linkedTokenSource.Token);
var completed = await Task.WhenAny(task, Task.Delay(_timeout));
if (completed != task)
{
linkedTokenSource.Cancel();//Try to cancel the method
throw new TimeoutException("The task did not complete within the TimeoutExecutionPolicy window of" + _timeout + "ms");
}
}
}
Using CancellationToken means that you're doing cooperative cancellation. Setting CancellationTokenSource.CancelAfter will transition the underlying token to a canceled state after the specified amount of time, but if that token isn't monitored by the calling async method, then nothing will happen.
In order for this to actually generate a OperationCanceledException, you need to call cts.Token.ThrowIfCancellationRequested inside _decoratedPolicy.ExecuteAsync.
For example:
// Assuming this is _decoratedPolicy.ExecuteAsync
public async Task ExecuteAsync(Action action, CancellationToken token)
{
// This is what creates and throws the OperationCanceledException
token.ThrowIfCancellationRequested();
// Simulate some work
await Task.Delay(20);
}
Edit:
In order to actually cancel the token, you need to monitor it at all points where work is executed and execution may timeout. If you cannot make that guarantee, then defer to #SriramSakthivel answer where the actual Task is being discarded, rather than being actually canceled.
You are calling Assert.Throws(Action action) and your anonymous async method is casted to async void. The method will be called asynchronously with Fire&Forget semantics without throwing exception.
However the process would likely crash shortly after due to the uncatched exception in an async void method.
You should call ExecuteAsync synchronously:
[Test]
public void Policy_TimeoutExpires_DoStuff_TaskShouldNotContinue()
{
var cts = new CancellationTokenSource();
var fakeService = new Mock<IFakeService>();
IExecutionPolicy policy = new TimeoutPolicy(new ExecutionTimeout(20), new DefaultExecutionPolicy());
Assert.Throws<AggregateException>(() => policy.ExecuteAsync(() => DoStuff(3000, fakeService.Object), cts.Token).Wait());
fakeService.Verify(f=>f.DoStuff(),Times.Never);
}
or use an async test method:
[Test]
public async Task Policy_TimeoutExpires_DoStuff_TaskShouldNotContinue()
{
var cts = new CancellationTokenSource();
var fakeService = new Mock<IFakeService>();
IExecutionPolicy policy = new TimeoutPolicy(new ExecutionTimeout(20), new DefaultExecutionPolicy());
try
{
await policy.ExecuteAsync(() => DoStuff(3000, fakeService.Object), cts.Token);
Assert.Fail("Method did not timeout.");
}
catch (TimeoutException)
{ }
fakeService.Verify(f=>f.DoStuff(),Times.Never);
}
I've decided to answer my own question here, as while each of the listed answers solved something that I needed to do, they did not identify the root cause of this issue. Many, many thanks to: Scott Chamberlain, Yuval Itzchakov,Sriram Sakthivel, Jeff Cyr. All advice gratefully received.
Root cause/Solution:
await Task.Factory.StartNew(action.Invoke, token);
which you see above in my "decorated policy" returns a Task and await waits only for the outer task. Replacing it with
await Task.Run(async () => await action.Invoke());
gets the correct result.
My code was suffering from a combination of Gotcha #4 and Gotcha #5 from an excellent article on C# async gotchas
The entire article (as well as answers posted to this question) has really improved my overall understanding.

Aborting a long running task in TPL

Our application uses the TPL to serialize (potentially) long running units of work. The creation of work (tasks) is user-driven and may be cancelled at any time. In order to have a responsive user interface, if the current piece of work is no longer required we would like to abandon what we were doing, and immediately start a different task.
Tasks are queued up something like this:
private Task workQueue;
private void DoWorkAsync
(Action<WorkCompletedEventArgs> callback, CancellationToken token)
{
if (workQueue == null)
{
workQueue = Task.Factory.StartWork
(() => DoWork(callback, token), token);
}
else
{
workQueue.ContinueWork(t => DoWork(callback, token), token);
}
}
The DoWork method contains a long running call, so it is not as simple as constantly checking the status of token.IsCancellationRequested and bailing if/when a cancel is detected. The long running work will block the Task continuations until it finishes, even if the task is cancelled.
I have come up with two sample methods to work around this issue, but am not convinced that either are proper. I created simple console applications to demonstrate how they work.
The important point to note is that the continuation fires before the original task completes.
Attempt #1: An inner task
static void Main(string[] args)
{
CancellationTokenSource cts = new CancellationTokenSource();
var token = cts.Token;
token.Register(() => Console.WriteLine("Token cancelled"));
// Initial work
var t = Task.Factory.StartNew(() =>
{
Console.WriteLine("Doing work");
// Wrap the long running work in a task, and then wait for it to complete
// or the token to be cancelled.
var innerT = Task.Factory.StartNew(() => Thread.Sleep(3000), token);
innerT.Wait(token);
token.ThrowIfCancellationRequested();
Console.WriteLine("Completed.");
}
, token);
// Second chunk of work which, in the real world, would be identical to the
// first chunk of work.
t.ContinueWith((lastTask) =>
{
Console.WriteLine("Continuation started");
});
// Give the user 3s to cancel the first batch of work
Console.ReadKey();
if (t.Status == TaskStatus.Running)
{
Console.WriteLine("Cancel requested");
cts.Cancel();
Console.ReadKey();
}
}
This works, but the "innerT" Task feels extremely kludgey to me. It also has the drawback of forcing me to refactor all parts of my code that queue up work in this manner, by necessitating the wrapping up of all long running calls in a new Task.
Attempt #2: TaskCompletionSource tinkering
static void Main(string[] args)
{ var tcs = new TaskCompletionSource<object>();
//Wire up the token's cancellation to trigger the TaskCompletionSource's cancellation
CancellationTokenSource cts = new CancellationTokenSource();
var token = cts.Token;
token.Register(() =>
{ Console.WriteLine("Token cancelled");
tcs.SetCanceled();
});
var innerT = Task.Factory.StartNew(() =>
{
Console.WriteLine("Doing work");
Thread.Sleep(3000);
Console.WriteLine("Completed.");
// When the work has complete, set the TaskCompletionSource so that the
// continuation will fire.
tcs.SetResult(null);
});
// Second chunk of work which, in the real world, would be identical to the
// first chunk of work.
// Note that we continue when the TaskCompletionSource's task finishes,
// not the above innerT task.
tcs.Task.ContinueWith((lastTask) =>
{
Console.WriteLine("Continuation started");
});
// Give the user 3s to cancel the first batch of work
Console.ReadKey();
if (innerT.Status == TaskStatus.Running)
{
Console.WriteLine("Cancel requested");
cts.Cancel();
Console.ReadKey();
}
}
Again this works, but now I have two problems:
a) It feels like I'm abusing TaskCompletionSource by never using it's result, and just setting null when I've finished my work.
b) In order to properly wire up continuations I need to keep a handle on the previous unit of work's unique TaskCompletionSource, and not the task that was created for it. This is technically possible, but again feels clunky and strange.
Where to go from here?
To reiterate, my question is: are either of these methods the "correct" way to tackle this problem, or is there a more correct/elegant solution that will allow me to prematurely abort a long running task and immediately starting a continuation? My preference is for a low-impact solution, but I'd be willing to undertake some huge refactoring if it's the right thing to do.
Alternately, is the TPL even the correct tool for the job, or am I missing a better task queuing mechanism. My target framework is .NET 4.0.
The real issue here is that the long-running call in DoWork is not cancellation-aware. If I understand correctly, what you're doing here is not really cancelling the long-running work, but merely allowing the continuation to execute and, when the work completes on the cancelled task, ignoring the result. For example, if you used the inner task pattern to call CrunchNumbers(), which takes several minutes, cancelling the outer task will allow continuation to occur, but CrunchNumbers() will continue to execute in the background until completion.
I don't think there's any real way around this other than making your long-running calls support cancellation. Often this isn't possible (they may be blocking API calls, with no API support for cancellation.) When this is the case, it's really a flaw in the API; you may check to see if there are alternate API calls that could be used to perform the operation in a way that can be cancelled. One hack approach to this is to capture a reference to the underlying Thread being used by the Task when the Task is started and then call Thread.Interrupt. This will wake up the thread from various sleep states and allow it to terminate, but in a potentially nasty way. Worst case, you can even call Thread.Abort, but that's even more problematic and not recommended.
Here is a stab at a delegate-based wrapper. It's untested, but I think it will do the trick; feel free to edit the answer if you make it work and have fixes/improvements.
public sealed class AbandonableTask
{
private readonly CancellationToken _token;
private readonly Action _beginWork;
private readonly Action _blockingWork;
private readonly Action<Task> _afterComplete;
private AbandonableTask(CancellationToken token,
Action beginWork,
Action blockingWork,
Action<Task> afterComplete)
{
if (blockingWork == null) throw new ArgumentNullException("blockingWork");
_token = token;
_beginWork = beginWork;
_blockingWork = blockingWork;
_afterComplete = afterComplete;
}
private void RunTask()
{
if (_beginWork != null)
_beginWork();
var innerTask = new Task(_blockingWork,
_token,
TaskCreationOptions.LongRunning);
innerTask.Start();
innerTask.Wait(_token);
if (innerTask.IsCompleted && _afterComplete != null)
{
_afterComplete(innerTask);
}
}
public static Task Start(CancellationToken token,
Action blockingWork,
Action beginWork = null,
Action<Task> afterComplete = null)
{
if (blockingWork == null) throw new ArgumentNullException("blockingWork");
var worker = new AbandonableTask(token, beginWork, blockingWork, afterComplete);
var outerTask = new Task(worker.RunTask, token);
outerTask.Start();
return outerTask;
}
}

Categories