Cancel Async operation - c#

private async void TriggerWeekChanged(Week currentWeek)
{
await LoadDataForSelectedWeek(currentWeek); //Split into multiple methods
}
In case a user hammers on the Change_Week Button how can I cancel the current Task, and start a new one with the new paramerters ?
I tried like this:
private async Task Refresh(Week selectedWeek, CancellationToken token)
{
Collection.Clear();
await LoadDataFromDatabase();
token.ThrowIfCancellationRequested();
await ApplyDataToBindings();
token.ThrowIfCancellationRequested();
//Do some other stuff
}
Problem is:
In my Collection I got data from multiple weeks when I hit the button to fast in a row.

Everything that is awaited will need to have visibility on that CancellationToken. If it is a custom Task that you wrote, it should accept it as an argument, and periodically within the function, check to see if it has been cancelled. If so, it should take any actions needed (if any) to stop or rollback the operation in progress, then call ThrowIfCancellationRequested().
In your example code, you propbably want to pass token in to LoadDataFromDatabase and ApplyDataToBindings, as well as any children of those tasks.
There may be some more advanced situations where you don't want to pass the same CancellationToken in to child tasks, but you still want them to be cancellable. In those cases, you should create a new, internal to the Task, Cancellationtoken that you use for child tasks.
An important thing to remember is that ThrowIfCancellationRequested marks safe places within the Task that the Task can be stopped. There is no guaranteed safe way for the runtime to automatically detect safe places. If the Task were to automatically cancel its self as soon as cancellation was requested, it could potentially be left in an unknown state, so it is up to developers to mark those safe locations. It isn't uncommon to have several calls to check cancellation scattered throughout your Task.
I've just notice that your TriggerWeekChanged function is async void. This is usually considered an anti-pattern when it is used on something that is not an event handler. It can cause a lot of problems with tracking the completed status of the async operations within the method, and handling any exceptions that might be thrown from within it. You should be very weary of anything that is marked as async void that isn't an event handler, as it is the wrong thing to do 99%, or more, of the time. I would strongly recommend changing that to async Task, and consider passing in the CancellationToken from your other code.

You did not mention about how LoadDataForSelectedWeek and Refresh interact.
For short you need to create one CancellationTokenSource instance that handle every click. Then pass it to that method and do Cancel method every time new one appear.
private async void TriggerWeekChanged(Week currentWeek, CancellationTokenSource tokenSource)
{
tokenSource.Cancel();
try
{
var loadDataTask = Task.Run(() => LoadDataForSelectedWeek(currentWeek, tokenSource.Token), tokenSource.Token); //Split into multiple methods
}
catch(OperationCanceledException ex)
{
//Cancelled
}
}
LoadDataForSelectedWeek -> Refresh (?)
private async Task Refresh(Week selectedWeek, CancellationToken token)
{
Collection.Clear();
await LoadDataFromDatabase();
token.ThrowIfCancellationRequested();
await ApplyDataToBindings();
token.ThrowIfCancellationRequested();
//Do some other stuff
}

Related

Sould I catch OperationCanceledException in OnParametersSetAsync/LifeCycle methods when cancelling Tasks inside it?

I have a blazor component (aka razor component, aka MyPage.razor file) with a #page directive. I'll call the thing/object "page" from here on.
It needs to await a Task (HTTP request simulated with Task.Delay) inside its OnParametersSetAsync().
When the page is left (user navigates somehwere else), awaited Tasks must be cancelled, so that there is no ping-ping (with data access) when thew new page is loaded and the old pages's Task finally finishes delayed. This is the reason for Dipose().
Since the Framework calls OnParametersSetAsync() rather than my own code, I'm not sure if I should let the OperationCanceledException simply bubble up (at finally probably be ignore afaik as the master of async said) - or if I should catch it and return gracefully from OnParametersSetAsync().
Is Blazor handeling cancellation from LifeCycle methods properly or is this the recommended way? Sadly the docu is very sparse. The example offers an button event handler, but IDK if that also counts for LifeCycle methods. But at least it seems it doesnt hurt the event handler (LongRunningWork), that it is not catched in user code.
I have testes both scenarios, and it seems either way, both work seemingly...
What I've noticed, is that even if the async Task OnParametersSetAsync() completes but another page is already active the the Task belongs to an already disosed page, no children LifeCycle methods are called anymore. The big question here is, is it "only" the C# user code in the remaining body of OnParametersSetAsync() that is executed delayed after the page has already been disposed - or will the successful completion of OnParametersSetAsync() trigger some other framework methods/events, even if the page has already been disposed, resulting in highly unpredictable behaviour? I'd also like to know that answer.
In any case, even if this would not cause problems, cancellation might still be important, so that at the end of the user code in OnParametersSetAsync() does not do any operations (e.g. on some data in some injected service or sth like that) that shouldn't be done anymore after disposing. So what's the right way?
Edit: Stephen said:
Ideally, you want to observe all your Task exceptions.
which is not possible, since OnParametersSetAsync() is called from the framework not from the user code, so I can't observe it inside the caller!
// MyPage.razor
<Child SomePara=#SomePara></Child>
//#code{
// ...
//private CancellationTokenSource cts = new();
//object SomePara = new();
// catch it?
protected override async Task OnParametersSetAsync()
{
Debug.WriteLine($"OnParametersSetAsync Start");
// sync stuff...
// async stuff:
try
{
await Task.Delay(5000, cts.Token);
await UnknownExternalTaskIWantToCancelAsync(cts.Token);
}
catch (Exception)
{
return; //??
throw; //??
}
//when cancel is requested, stop here, this component is being disposed and should do as little as possible, especially nothing async and should't inform children to render
//when cancel is requested, while above Tasks are awaited, all below code MUST NOT run
// ??
//cts.Token.ThrowIfCancellationRequested();
Debug.WriteLine($"OnParametersSetAsync End");
// stuff I don't want do be done after cancelled
}
// let it bubble up?
protected override async Task OnParametersSetAsync()
{
Debug.WriteLine($"OnParametersSetAsync Start");
// sync stuff...
// async stuff:
await Task.Delay(5000, cts.Token);
await UnknownExternalTaskIWantToCancelAsync(cts.Token);
//when cancel is requested, stop here, this Razor component is being disposed and should do as little as possible, especially nothing async and should't inform children to render
//when cancel is requested, while above Tasks are awaited, all below code MUST NOT run
// ??
//cts.Token.ThrowIfCancellationRequested();
Debug.WriteLine($"OnParametersSetAsync End");
// stuff I don't want do be done after cancelled
}
public void Dispose()
{
Debug.WriteLine($"Disposing");
cts.Cancel();
cts.Dispose();
}
async Task UnknownExternalTaskIWantToCancelAsync(CancellationToken cts)
{
//This could be a HTTP call or some other Task.
Debug.WriteLine($" . . . . . START.");
await Task.Delay(10000, cts);
Debug.WriteLine($" . . . . . FINISHED.");
}
//}
One could imagine also a pretty hacky idea, but thats prolly bad:
// Hacky option ?????
bool isCancelled = false;
protected override async Task OnParametersSetAsync()
{
Debug.WriteLine($"OnParametersSetAsync Start");
// sync stuff...
// async stuff:
await Task.Delay(5000);
await UnknownExternalTaskIWantToCancelAsync(cts.Token);
//when cancel is requested, stop here, this Razor component is being disposed and should do as little as possible, especially nothing async and should't inform children to render
if (isCancelled)
{
return;
}
Debug.WriteLine($"OnParametersSetAsync End");
// stuff I don't want do be done after cancelled
}
public void Dispose()
{
Debug.WriteLine($"Disposing");
isCancelled = true ;
}
Update:
In the link I provided above, the official docs DON'T CATCH the exception in LongRunningWork !! Which leads to a Console msg:
Exception thrown: 'System.Threading.Tasks.TaskCanceledException' in System.Private.CoreLib.dll
A task was canceled.
Question: Is it ok to do it exactly like that with LifeCycle methods, specifically OnParametersSetAsync() without wrapping a try-catch block around the Task to catch the OperationCanceledException which is legit way a CTS cancels the Task. As the nature of an exception, it bubbles up until catched. OperationCanceledException are special and won't crash the App, but is it ok to let it bubble up in a Blazor LifeCycle method?? LongRunningWork is NOT a Blazor LifeCycle method, but only a event handler for a button click event. Is it ok to treat a LifeCycle methods the same way in this regard??
Update:
I've read several posts about Task cancellation aswell as the official docs, but none answers the specific case for the Blazor LifeCycle methods, like OnParametersSetAsync.
Links:
Regarding asynchronous Task, why is a Wait() required to catch OperationCanceledException?
How to properly cancel Task and catch OperationCanceledException?
How to cancel a Task using CancellationToken?
Elegantly handle task cancellation
How to Correctly Cancel a TPL Task with Continuation
Cancelling a Task is throwing an exception
Please try to refer to make code examples of "catch it?" or "let it bubble up?" in your answers, thanks very much.
You can use CancellationTokenSource.Object that creates a cancellation token, and also issues the cancellation request for all copies of that token. Below is a work demo, you can refer to it.
protected override async Task OnParametersSetAsync()
{ // Create the token source.
CancellationTokenSource cts = new CancellationTokenSource();
// Pass the token to the cancelable operation.
ThreadPool.QueueUserWorkItem(new WaitCallback(DoSomeWork), cts.Token);
// Request cancellation.
cts.Cancel();
Console.WriteLine("Cancellation set in token source...");
Thread.Sleep(2500);
// Cancellation should have happened, so call Dispose.
cts.Dispose();
static void DoSomeWork(object obj)
{
CancellationToken token = (CancellationToken)obj;
for (int i = 0; i < 100000; i++)
{
if (token.IsCancellationRequested)
{
Console.WriteLine("In iteration {0}, cancellation has been requested...",
i + 1);
// Perform cleanup if necessary.
//...
// Terminate the operation.
break;
}
// Simulate some work.
Console.WriteLine(i.ToString());
Thread.Sleep(1000);
}
}
}
result:
If remove cts.Cancel(); ,you will see it will go on.
Read Cancellation in Managed Threads to know more.

Execution of ContinueWith when the antecedent Task is in canceled state and usage of async delegate inside of ContinueWith

The code discussed here is written in C# and executed with .netcore 3.1
I have the following piece of code, which starts a workload in the background without awaiting for it to complete (fire and forget):
public void StartBackgroundWork(IAsyncDisposable resource, CancellationToken token)
{
// some background work is started in a fire and forget manner
_ = Task.Run(async () =>
{
try
{
// here I perform my background work. Regardless of the outcome resource must be released as soon as possible
// I want that cancellation requests coming from the provided cancellation token are correctly listened by this code
// So, I pass the cancellation token everywhere
await Task.Delay(1500, token);
}
finally
{
// here I need to release the resource. Releasing this resource is important and must be done as soon as possible
await resource.DisposeAsync();
}
}, token);
}
There are three important points:
the background work is started in a fire and forget manner. I'm not interested in awaiting its completion
the provided cancellation token is important and the background work must listed to incoming cancellation requests
the provided resource (IAsyncDisposable) must be released as soon as possible, regardless of the outcome of the background work. In order to release the resource a call to DisposeAsync is required.
The problem with this code is that the cancellation token is passed to Task.Run invokation. If the token is canceled before the execution of the async delegate starts, the async delegate is never executed and so the finally block is never executed. By doing so the requirement of releasing the IAsyncDisposable resource is not met (basically, DisposeAsync is never called).
The simplest way to solve this issue is not providing the cancellation token when Task.Run is invoked. That way, the async delegate is always executed and so the finally block is executed too. The code inside the async delegate listens to cancellation requests, so the requirement of cancel the execution is met too:
public void StartBackgroundWork(IAsyncDisposable resource, CancellationToken token)
{
// some background work is started in a fire and forget manner
_ = Task.Run(async () =>
{
try
{
// here I perform my background work. Regardless of the outcome resource must be released as soon as possible
// I want that cancellation requests coming from the provided cancellation token are correctly listened by this code
// So, I pass the cancellation token everywhere
await Task.Delay(1500, token);
}
finally
{
// here I need to release the resource. Releasing this resource is important and must be done as soon as possible
await resource.DisposeAsync();
}
}, CancellationToken.None);
}
I'm asking myself whether the release of the IAsyncDisposable resource should, instead, be delegated to a continuation task. The code refactored by using this approach is the following:
public void StartBackgroundWork(IAsyncDisposable resource, CancellationToken token)
{
// some background work is started in a fire and forget manner
_ = Task.Run(async () =>
{
// here I perform my background work. Regardless of the outcome resource must be released as soon as possible
// I want that cancellation requests coming from the provided cancellation token are correctly listened by this code
// So, I pass the cancellation token everywhere
await Task.Delay(1500, token);
},
token).ContinueWith(async _ =>
{
// release the IAsyncDisposable resource here, afte the completion of the antecedent task and regardless
// of the antecedent task actual state
await resource.DisposeAsync();
});
}
I'm not really familiar with ContinueWith gotchas, so my questions are the following:
do I have the guarantee that the continuation is always executed, even if the cancellation token is canceled before the execution of the antecedent task starts ?
is there any issue in providing an async delegate to the invokation of ContinueWith ? Is the execution of the async delegate fully completed as expected ?
What is the best approach ? Passing CancellationToken.None to the invokation of Task.Run, or relying on the continuation by using ContinueWith ?
IMPORTANT NOTE: I know that using Task.Run is not the best approach in a server application (more on that can be found here), so there are probably much better ways of designing my overall architecture. I posted this question to better understanding the actual behavior of ContinueWith, because I'm not really familiar with its usage (in modern .NET code it is largely replaced by the usage of async await).
You could consider using the await using statement, that handles the asynchronous disposal of the resource automatically:
public async void StartBackgroundWork(IAsyncDisposable resource, CancellationToken token)
{
await using var _ = resource;
try
{
await Task.Run(async () =>
{
await Task.Delay(1500, token);
}, token);
} catch (OperationCanceledException) { }
}
I also converted your fire-and-forget task to an async void (aka fire-and-crash) method. In case the unthinkable happens and your code has a bug, instead of the app continue running with an unobserved exception having occurred, resulting possibly to corrupted application state, the whole app will crash, forcing you to fix the bug ASAP.
But honestly creating a disposable resource in one method and disposing it in another is a smelly design. Ideally the method that created the resource should be responsible for disposing it finally.
I think Theodor has a great answer; I'm just going to answer some of your other questions:
do I have the guarantee that the continuation is always executed, even if the cancellation token is canceled before the execution of the antecedent task starts ?
ContinueWith will execute its delegate even of the antecedent task is already completed. In this specific case, there is no "guarantee" simply because of the nature of fire-and-forget.
is there any issue in providing an async delegate to the invokation of ContinueWith ?
ContinueWith is not async-aware, so the return type of ContinueWith is surprising for most developers. Since your code discards the return type, that's not a concern here.
Is the execution of the async delegate fully completed as expected ?
In this case, most likely, but it really depends on what "expected" means. Like all other fire-and-forget code, you can't guarantee completion. ContinueWith has an additional wrinkle: it executes its delegate using a TaskScheduler, and the default TaskScheduler is not TaskScheduler.Default but is actually TaskScheduler.Current. So I always recommend passing an explicit TaskScheduler for clarity if you really need to use ContinueWith.
What is the best approach ? Passing CancellationToken.None to the invokation of Task.Run, or relying on the continuation by using ContinueWith ?
Just drop the second argument to Task.Run.
I'll go further than that: Task.Run probably shouldn't even take a CancellationToken. I have yet to see a scenario where it's useful. I suspect the CancellationToken part of the API was copied from TaskFactory.StartNew (where it is rarely useful), but since Task.Run always uses TaskScheduler.Default, providing a CancellationToken is not useful in practice.
P.S. I recently wrote a short series on the proper solution for fire-and-forget on ASP.NET.

What is a safe way to wait synchronously with cancellation?

I'm writing a .NET Standard 2.0 library that will have both synchronous and asynchronous versions of the same functionality, one of the features requires a delay with cancellation support.
I'm trying to come up with a way to wait a specific amount of time that would work without deadlocks or other gotchas in all scenarios. One worry is that waiting synchronously on async methods can cause deadlocks.
Consider this class, how should you implement Wait to make it safe anywhere? Calling WaitAsync is not a requirement, the wait could be implemented completely separately.
class Waiter
{
// Easy enough
public async Task WaitAsync(TimeSpan delay, CancellationToken token = default)
{
try
{
await Task.Delay(delay, token)
}
catch(OperationCancelledException)
{
}
}
// Not so straightforward
public void Wait(TimeSpan delay, CancellationToken token = default)
{
WaitAsync(delay, token).GetAwaiter().GetResult(); // May deadlock
WaitAsync(delay, token).Wait(); // May deadlock, also doesn't propagate exceptions properly
WaitAsync(delay).Wait(token); // Even worse
Thread.Sleep(delay) // Doesn't support cancellation
token.WaitHandle.WaitOne(delay, true); // Maybe? Not sure how the second parameter works when I have no control over the context
token.WaitHandle.WaitOne(delay, false); // No idea
}
}
This answer almost answers all your question. Specifically, it explains that implementing sync and async APIs by having one call the other is not a good idea. If you're in this situation, I recommend the boolean argument hack.
For your specific problem, ideally you would use a synchronous wait (Thread.Sleep), but since it doesn't support CancellationToken that's not an option. So you'd need to write it yourself. As noted in your question, WaitHandle has a timeout parameter which you can use:
public void Wait(TimeSpan delay, CancellationToken token = default)
{
token.WaitHandle.WaitOne(delay);
}
It's worthwhile taking a look at how Polly handles this, and indeed, they implement their synchronous cancelable delay in this way:
public static Action<TimeSpan, CancellationToken> Sleep = (timeSpan, cancellationToken) =>
{
if (cancellationToken.WaitHandle.WaitOne(timeSpan))
cancellationToken.ThrowIfCancellationRequested();
};

C# Task.Factory.CancellationToken

My question is about task cancellation. I have to poll the token.IsCancellationRequested to detect a cancellation. I call cts.Cancel(); in a WindowsForm Buttonmethod.
Questions:
If I hit the Button is the CancelRequest stored? Or do I have to be lucky, that to same time when I press my Button the code is at the position if (token.IsCancellationRequested)?
Is it possible to cancel my Task with for-loop by event?
Code Example:
CancellationTokenSource cts = new CancellationTokenSource();
CancellationToken token = cts.Token;
Task t1 = Task.Factory.StartNew(() =>
{
// Do syncronius work
for(int i=0; i<1000;++i)
{
DoSyncWork(i);
if (token.IsCancellationRequested)
{
Console.WriteLine("Cancelled");
break;
}
Thread.Sleep(1000);
}
});
The cancellation request is a one time thing. Once a token source is canceled it can never be un-canceled so all derived tokens will have IsCancellationRequested always return true.
Yes it is possible, but for a for loop I don't think it is a better way. The way you use a event is you pass the callback to the CancellationToken.Register method and the callback is your event. I leave it to you how you would make a Action delegate cancel the for loop.
A few things with your code that you did not bring up:
You should never call Task.Factory.StartNew without passing in TaskScheduler, if you don't you could cause your code in the StartNew to run on the UI thread when you expect it to be on a background thread. Either use Task.Run or make sure you pass in a scheduler (TaskScheduler.Default is the one Task.Run( uses to always run on the background thread, TaskScheduler.Current is the one that is used when you don't pass anything in and is the one that can cause stuff to run on the UI thread).
If you pass the token in to the factory (or to Task.Run() then use token.ThrowIfCancellationRequested() this will cause the task to enter the Canceled state instead of the Completed state (if you forget to pass it to the factory it will enter the Faulted state), this can be useful for when you need to know when the task finished or not when you are awaiting.
The cancellation is "stored". If you call Cancel() on your CancellationTokenSource instance, the IsCancellationRequested property of the CancellationToken will be true for the rest of its existence.
As I understand it, you want to break your for loop by an event? I don't know how this should look like. The control flow of a for loop is straight forward, no event could break that. But you can use the token in the for loop's header:
for(int i=0; i<1000 && !token.IsCancellationRequested; ++i)
{
...
}
// output log if cancelled
if (token.IsCancellationRequested) Console.WriteLine(...);
if it's that what you want.
The usual implementation for cancelling out with a cancellation token is to throw the OperationCanceledException, by using the tokens .ThrowIfCancellationRequested. This allows you to catch a cancelled operation and get out of operating for however deep you are in the stack and know that it was cancelled.
For your first question, as soon as the token has been cancelled, cancellation will be requested and when you come back around in the loop the if block would be true that you have. Instead of that though I would just use token.ThrowIfCancellationRequested, and catch the specific OperationCanceledException and do any logging you want.
Second question, you can register a cancellation from anything that can access to your cancellationtokensource by calling cancel. So any event that is able to access the cancellationtokensource you could call it's cancellation event. I will often put a tokensource as an instance variable on a form that should support cancellation so that a "cancel" button or some other event that causes cancellation can call on the cancel method for the cts.
Example of one way I'll set up a form with a token:
public class MyForm
{
private CancellationTokenSource _cts;
private void Cancel()
{
if (_cts != null) {
_cts.Cancel();
}
}
}

Stop and resume async calls

When navigating to page, i'm calling in the viewmodel
public void OnNavigatedTo()
{
ThreadPool.QueueUserWorkItem(async o =>
{
collectionsAnswer = await productCategoriesDataService.RequestServerAsync();
***
if (collectionsAnswer.status == Ok)
{
var parsedList = await productCategoriesDataService.Parse(collectionsAnswer.json);
_itemsList = new List<ProductItem>(parsedList);
DispatcherHelper.CheckBeginInvokeOnUI(() =>
RaisePropertyChanged("ItemsList", _itemsList, _itemsList, true));
}
}
How to stop/resume it properly? I tried to Abort current HttpWebResponse (which is inside of RequestServerAsync() ) from OnNavigatedFrom(), but it crashed, when i'm returning to the same page again.
So, in short, the problem is:
Navigating to page starts await commands
Leaving the page (by pressing Back) should cancel current request
Visiting page should create new request, but awaits are still waiting (if to return fast enough)
Are there better ways to solve this problem? Maybe i should create new instances of viewmodels every time?
Cancellations in the TPL and in async-await code are performed through the use of CancellationTokenSource and CancellationToken. Many asynchronous methods have an overload which accepts a CancellationToken as a parameter, this is then used by the method to observe for cancellation.
Here is a post on the MSDN about how to initiate and handle the cancellation of async tasks.
When using cancellation tokens, I would advise the use of this overload of Task.Run which takes a CancellationToken parameter instead of ThreadPool.QueueUserWorkItem. The token parameter is used internally by the Task to return a cancelled task upon cancellation, failing to use this overload could result in an OperationCanceledException being thrown from Task.Run.

Categories

Resources