Cancel all but last task - c#

How do I cancel all but the last/latest task? For example, suppose I have a time-consuming task that is triggered on a button click. I only want the task from the last button click to run and the previous ones to cancel. Can you show me how this is normally done?
My attempt involves storing all tasks in a list along with their cancellation token and removing them when either the task completes or is cancelled. Creating a list to store both Task and CancellationToken seems like I'm doing too much for what I would've thought is a common requirement (eg. a User does a search for something and clicks the search button multiple times. Shouldn't only the last search be made and all other cancelled?). This is a common scenario, so I'd like to know how this is normally done. What is best practice here?
async void DoStuffAsync()
{
// Store tasks in a list
if (tasksAndCancelTokens == null)
tasksAndCancelTokens = new List<Tuple<Task, CancellationTokenSource>>();
else // we have a new Get request so cancel any previous
tasksAndCancelTokens.ForEach(t => t.Item2.Cancel());
// Create Cancellation Token
var cts = new CancellationTokenSource();
// Method to run asynchonously
Func<int> taskAction = () =>
{
// Something time consuming
Thread.Sleep(5000);
if (cts.Token.IsCancellationRequested)
cts.Token.ThrowIfCancellationRequested();
return 100;
};
// Create Task
Task<int> task = Task<int>.Factory.StartNew(taskAction, cts.Token);
// Create Tuple to store task in list
var tup = new Tuple<Task, CancellationTokenSource>(task, cts);
tasksAndCancelTokens.Add(tup);
try
{
int i = await task;
}
catch (OperationCanceledException)
{
// Don't need to do anything
}
finally
{
tasksAndCancelTokens.Remove(tup);
}
}
Thanks

If you just want to cancel the last task, then that's all you have to do:
CancellationTokenSource cts = null;
async void Button1_Click(...)
{
// Cancel the last task, if any.
if (cts != null)
cts.Cancel();
// Create Cancellation Token for this task.
cts = new CancellationTokenSource();
var token = cts.Token;
// Method to run asynchonously
Func<int> taskAction = () =>
{
// Something time consuming
Thread.Sleep(5000);
token.ThrowIfCancellationRequested();
return 100;
};
try
{
int i = await Task.Run(taskAction);
}
catch (OperationCanceledException)
{
// Don't need to do anything
return;
}
}

Related

Add tasks to WhenAny asynchronously

I'm looking to listen to the result of a task(s), or timeout if the none are made or completed. Specifically, I'm using Playwright and am adding an event listener for requests and want to timeout if none of those requests include what I'm looking for (or even get created). Example code below:
I've tried looking into WhenAny, but that requires knowing the tasks ahead of time. Also looked into Monitor.TryEnter, but not sure how to have the requests acquire the lock before they're generated? Maybe a semaphore or something?
string? result = null;
// Start listening for new pages (ClickAsync below opens in a new page)
context.Page += (_, newPage) =>
{
// From the new page, start listening for requests that page generates
newPage.Request += async (_, request) =>
{
if (request.Url.Contains("some_identifier"))
{
await newPage.GotoAsync(request.Url);
result = await newPage.InnerTextAsync("body");
// Do something here? We now have the result we want, stop waiting and return it!
}
};
};
await mainPage.Locator("a#somelink").ClickAsync();
// Wait for *up to* 30 seconds here.
return result;
Using ideas from comments on my answer I was able to accomplish the goal. I ended up passing a cancellation token to a Task.Delay(30000, token). Then if I got the data I wanted and set the result value, I just cancelled the token.
string? result = null;
var tokenSource = new CancellationTokenSource();
var cancelToken = tokenSource.Token;
// Start listening for new pages (ClickAsync below opens in a new page)
context.Page += (_, newPage) =>
{
// From the new page, start listening for requests that page generates
newPage.Request += async (_, request) =>
{
if (request.Url.Contains("some_identifier"))
{
await newPage.GotoAsync(request.Url);
result = await newPage.InnerTextAsync("body");
tokenSource.Cancel(); // <-- Got what we needed, cancel 30 second timer
}
};
};
await mainPage.Locator("a#somelink").ClickAsync();
try {
await Task.Delay(30000, cancelToken); // <-- Timeout after 30 seconds.
}
catch
{
// Not a fan of tokenSource.Cancel() throwing an exception, but I can deal with it.
}
return result;

Cancel Task running under User Control

In my project I have few User controls changed by navigation. One of controls runs Tasks. I do it like this:
public partial class uc_WorkingArea : UserControl
{
CancellationTokenSource cts = new CancellationTokenSource();
CancellationToken token;
public uc_WorkingArea()
{
InitializeComponent();
this.Unloaded += uc_WorkingArea_Unloaded;
token = cts.Token;
Task Printer1 = Task.Run(() => PrinterPooler(lst_PrinterStruct, 0), cts.Token);
}
private void uc_WorkingArea_Unloaded(object sender, RoutedEventArgs e)
{
cts.Cancel();
if (token.CanBeCanceled)
{
MessageBox.Show("Can be canceled");
}
if (token.IsCancellationRequested)
{
MessageBox.Show("Canceled requested");
}
cts.Cancel();
MessageBox.Show("Status: " + Printer1.Status.ToString());
}
}
When I leave current user control and switching to another uc_WorkingArea_Unloaded executes. I see messages, that Task can be canceled and request to cancel accepted.
But, current status of Printer1 task still "IsRunning".
So, If I return back to this user control, Task starts again and Application had two running similar tasks.
I tried run task under Factory, like this
Task Printer1 = Task.Factory.StartNew(() => PrinterPooler(lst_PrinterStruct, 0), cts.Token);
But without success. App still runs two similar tasks.
PrinterPooler method not async.
I can't understand where mistake was made. Your help guys needed.
You have to pass the token into the PrintPooler method, and there inside check if it should be cancelled.
for(int i = 0; i < 10000; i++)
{
DoStuff();
cancelToken.ThrowIfCancellationRequested(); // if tasks end with this exception, it knows the work has been cancelled
}
Canceling a Task does not stop the execution, it only gives signal to code inside that it should end and sets the task status to Cancelled/Faulted/RanToCompletion depending on how execution stops.
Note that you need to pass the same token to the Task and to the method that will throw it.
Regarding to this post How do I abort/cancel TPL Tasks?
You have to Implement your cancle condition by your self. For example:
public partial class uc_WorkingArea : UserControl
{
public CancellationTokenSource cts = new CancellationTokenSource();
public CancellationToken token;
public Task Printer1;
public uc_WorkingArea()
{
token = cts.Token;
Printer1 = Task.Factory.StartNew(() =>
{
while (!token.IsCancellationRequested)
{
Console.WriteLine("run");
Application.DoEvents();
}
}, token);
}
}
Cancel Call:
uc_WorkingArea gc = new uc_WorkingArea();
for (int i = 0; i < 10; i++) //PASS SOME TIME
{
Application.DoEvents(); //CONSOLE SHOULD SPAM 'RUN' FROM TASK
Thread.Sleep(1);
}
gc.cts.Cancel(); //CANCEL CALL, WHILE LOOP END
if (gc.token.IsCancellationRequested)
{
Console.WriteLine("stop");
MessageBox.Show("Canceled requested");
}
gc.cts.Dispose();
gc.Printer1.Dispose();
Hope it helps.

C# How to handle cancel task with eventhandler inside

I have requirement to update ui control when status of dependent service will change. I have this sample code, which polling service api to get status and sends result to recalculate and update ui by main thread:
public void StartObserving() {
this.cts = new CancellationTokenSource();
this.cts.Token.ThrowIfCancellationRequested();
this.isRunning = true;
var token = this.cts.Token;
Task.Run(async () =>
{
try
{
while (this.isRunning)
{
var result = this.serviceAPI.GetStatus();
this.OnServiceStatusChanged(result);
await Task.Delay(3000);
}
}
catch (OperationCanceledException)
{
this.isRunning = false;
}
catch (Exception ex)
{
this.isRunning = false;
this.logger.LogError(ex);
}
}, token);
}
And the problem is when I want to cancel above Task. When I call this.cts.Cancel() in another method in this class, I get Exception 'A task was canceled' on dispatcher which was triggered by EventHandler: OnServiceStatusChanged
How I should properly implement this scenario?
I would simply check whether the token in cancelled in the inner loop, and exit the loop if it is. No need to pass the token to the Task.Run() method.
public void StartObserving()
{
this.cts = new CancellationTokenSource();
var token = this.cts.Token;
Task.Run(async () =>
{
try
{
while (!token.IsCancellationRequested)
{
var result = this.serviceAPI.GetStatus();
this.OnServiceStatusChanged(result);
await Task.Delay(3000);
}
}
catch
{
}
});
}
Tried to simulate this behavior in a console app. Task started, but after calling cts.Cancel(), the task continues to execute... Very strange.
However, I could cancel the task by simply setting this.isRunning to false (instead of calling cts.Cancel()). But I am not sure if this is the solution you want.
If serviceAPI.GetStatus() is a blocking call that waits indeffinitly, then you cannot properly cancel this task.
Proper cancellation of async methods involves marking safe cancellation points with CancellationToken.ThrowIfCancellationRequested().
You would have to rewrite serviceAPI.GetStatus() as an async method that you await the result of. It should contain calls to CancellationToken.ThrowIfCancellationRequested() at points where it can be safely cancelled. You would want to pass the cancellation token in to both that method, and the call to Task.Delay() for optimal performance.

How to handle task cancellation in the TPL

Good day! I am writing a helper library for WinForms UI. Started using TPL async/await mechanism and got a problem with this kind of code example :
private SynchronizationContext _context;
public void UpdateUI(Action action)
{
_context.Post(delegate { action(); }, null);
}
private async void button2_Click(object sender, EventArgs e)
{
var taskAwait = 4000;
var progressRefresh = 200;
var cancellationSource = new System.Threading.CancellationTokenSource();
await Task.Run(() => { UpdateUI(() => { button2.Text = "Processing..."; }); });
Action usefulWork = () =>
{
try
{
Thread.Sleep(taskAwait);
cancellationSource.Cancel();
}
catch { }
};
Action progressUpdate = () =>
{
int i = 0;
while (i < 10)
{
UpdateUI(() => { button2.Text = "Processing " + i.ToString(); });
Thread.Sleep(progressRefresh);
i++;
}
cancellationSource.Cancel();
};
var usefulWorkTask = new Task(usefulWork, cancellationSource.Token);
var progressUpdateTask = new Task(progressUpdate, cancellationSource.Token);
try
{
cancellationSource.Token.ThrowIfCancellationRequested();
Task tWork = Task.Factory.StartNew(usefulWork, cancellationSource.Token);
Task tProgress = Task.Factory.StartNew(progressUpdate, cancellationSource.Token);
await Task.Run(() =>
{
try
{
var res = Task.WaitAny(new[] { tWork, tProgress }, cancellationSource.Token);
}
catch { }
}).ConfigureAwait(false);
}
catch (Exception ex)
{
}
await Task.Run(() => { UpdateUI(() => { button2.Text = "button2"; }); });
}
Basically, the idea is to run two parallel tasks - one is for, say, progress bar or whatever update and a sort of timeout controller, the other is the long running task itself. Whichever task finishes first cancels the other one. So, there should not be a problem to cancel the "progress" task as it has a loop in which I can check if task is marked cancelled. The problem is with the long running one. It could be Thread.Sleep() or SqlConnection.Open(). When I run CancellationSource.Cancel(), the long running task keeps working and does not cancel. After a timeout I am not interested in long running task or whatever it may result in.
As the cluttered code example may suggest, I have tried a bunch of variants and none given me a desired effect. Something like Task.WaitAny() freezes UI... Is there a way to make that cancellation work or may be even a different approach to code these things?
UPD:
public static class Taskhelpers
{
public static async Task<T> WithCancellation<T>(this Task<T> task, CancellationToken cancellationToken)
{
var tcs = new TaskCompletionSource<bool>();
using (cancellationToken.Register(s => ((TaskCompletionSource<bool>)s).TrySetResult(true), tcs))
{
if (task != await Task.WhenAny(task, tcs.Task))
throw new OperationCanceledException(cancellationToken);
}
return await task;
}
public static async Task WithCancellation(this Task task, CancellationToken cancellationToken)
{
var tcs = new TaskCompletionSource<bool>();
using (cancellationToken.Register(s => ((TaskCompletionSource<bool>)s).TrySetResult(true), tcs))
{
if (task != await Task.WhenAny(task, tcs.Task))
throw new OperationCanceledException(cancellationToken);
}
await task;
}
}
.....
var taskAwait = 4000;
var progressRefresh = 200;
var cancellationSource = new System.Threading.CancellationTokenSource();
var cancellationToken = cancellationSource.Token;
var usefulWorkTask = Task.Run(async () =>
{
try
{
System.Diagnostics.Trace.WriteLine("WORK : started");
await Task.Delay(taskAwait).WithCancellation(cancellationToken);
System.Diagnostics.Trace.WriteLine("WORK : finished");
}
catch (OperationCanceledException) { } // just drop out if got cancelled
catch (Exception ex)
{
System.Diagnostics.Trace.WriteLine("WORK : unexpected error : " + ex.Message);
}
}, cancellationToken);
var progressUpdatetask = Task.Run(async () =>
{
for (var i = 0; i < 25; i++)
{
if (!cancellationToken.IsCancellationRequested)
{
System.Diagnostics.Trace.WriteLine("==== : " + i.ToString());
await Task.Delay(progressRefresh);
}
}
},cancellationToken);
await Task.WhenAny(usefulWorkTask, progressUpdatetask);
cancellationSource.Cancel();
By modifying for (var i = 0; i < 25; i++) limit of i I imitate whether long running task finishes before the progress task or otherwise. Works as desired. The WithCancellation helper method does the job, although two sort of 'nested' Task.WhenAny look suspicious for now.
I agree with all the points in Paulo's answer - namely, use modern solutions (Task.Run instead of Task.Factory.StartNew, Progress<T> for progress updates instead of manually posting to the SynchronizationContext, Task.WhenAny instead of Task.WaitAny for asynchronous code).
But to answer the actual question:
When I run CancellationSource.Cancel(), the long running task keeps working and does not cancel. After a timeout I am not interested in long running task or whatever it may result in.
There are two parts to this:
How do I write code that responds to a cancellation request?
How do I write code that ignores any responses after the cancellation?
Note that the first part deals with cancelling the operation, and the second part is actually dealing with cancelling the waiting for the operation to complete.
First things first: support cancellation in the operation itself. For CPU-bound code (i.e., running a loop), periodically call token.ThrowIfCancellationRequested(). For I/O-bound code, the best option is to pass the token down to the next API layer - most (but not all) I/O APIs can (should) take cancellation tokens. If this isn't an option, then you can either choose to ignore the cancellation, or you can register a cancellation callback with token.Register. Sometimes there's a separate cancellation method you can call from your Register callback, and sometimes you can make it work by disposing the object from the callback (this approach often works because of a long-standing Win32 API tradition of cancelling all I/O for a handle when that handle is closed). I'm not sure if this will work for SqlConnection.Open, though.
Next, cancelling the wait. This one is relatively simple if you just want to cancel the wait due to a timeout:
await Task.WhenAny(tWork, tProgress, Task.Delay(5000));
When you write something like await Task.Run(() => { UpdateUI(() => { button2.Text = "Processing..."; }); }); on your button2_Click, you are, from the UI thread, scheduling an action to a thread poll thread that posts an action to the UI thread. If you called the action directly, it would be quickier because it wouldn't have two context switchings.
ConfigureAwait(false) causes the synchronization context to not being captured. I should not be used inside UI methods because, you most certainely, want to do some UI work on the continuation.
You shouldn't use Task.Factory.StartNew instead of Task.Run unless you absolutely have a reason to. See this and this.
For progress updates, consider using the Progress<T> class, because it captures the synchronization context.
Maybe you should try something like this:
private async void button2_Click(object sender, EventArgs e)
{
var taskAwait = 4000;
var cancellationSource = new CancellationTokenSource();
var cancellationToken = cancellationSource.Token;
button2.Text = "Processing...";
var usefullWorkTask = Task.Run(async () =>
{
try
{
await Task.Dealy(taskAwait);
}
catch { }
},
cancellationToken);
var progress = new Progress<imt>(i => {
button2.Text = "Processing " + i.ToString();
});
var progressUpdateTask = Task.Run(async () =>
{
for(var i = 0; i < 10; i++)
{
progress.Report(i);
}
},
cancellationToken);
await Task.WhenAny(usefullWorkTask, progressUpdateTask);
cancellationSource.Cancel();
}
I think you need to check IsCancellationRequested in the progressUpdate Action.
As to how to do what you want, this blog discusses an Extension method WithCancellation that will make it so that you stop waiting for your long running task.

Task cancelling for an autocomplete field doesn't cancel all previous tasks

I have a search method that returns search suggestions to the UI. This method is fired every time the user enters a new character into the search box.
I have added some cancellation code to cancel the previous search request. This works but not all the time.
private CancellationTokenSource cancellationTokenSource;
private async Task UserSearch(string searchCriteria)
{
Debug.WriteLine("Searching for {0}....", searchCriteria);
try
{
var cts = new CancellationTokenSource();
this.Suggestions = await this.SearchAsync(searchCriteria, cts.Token);
}
catch (OperationCanceledException)
{
Debug.WriteLine("Search({0}) cancelled", searchCriteria);
}
}
private async Task<IList<string>> SearchAsync(string searchCriteria, CancellationToken cancelToken)
{
CancellationTokenSource previousCts = this.cancellationTokenSource;
CancellationTokenSource linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancelToken);
this.cancellationTokenSource = linkedCts;
// if previous task running cancel it
if (previousCts != null)
{
previousCts.Cancel();
}
linkedCts.Token.ThrowIfCancellationRequested();
List<string> results =
(await this.searchProvider.SearchAsync(searchCriteria, linkedCts.Token)).ToList();
Debug.WriteLine("Search({0}) returned {1} records", searchCriteria, results.Count);
linkedCts.Dispose();
this.cancellationTokenSource = null;
return results;
}
For example. I get the following debug messages:
SearchTerm changing to: Di
Searching for Di....
SearchTerm changing to: Dia
Searching for Dia....
Search(Di) cancelled
SearchTerm changing to: Diap
Searching for Diap....
Search(Diap) returned 323 records
Search(Dia) returned 3230 records
As you can see the first search gets cancelled but the second one doesn't and it gets returned after the last search giving incorrect results to the user.
How can I ensure previous tasks are always cancelled?
I think you are probably complicating your solution a little. All you need to do is see if there is an existing operation in progress that hasn't been cancelled and cancel it. Then perform the new search. Untested, but I think this should do it.
private CancellationTokenSource cancellationTokenSource;
private async Task UserSearch(string searchCriteria)
{
Debug.WriteLine("Searching for {0}....", searchCriteria);
try
{
if(cancellationTokenSource != null &&
!cancellationTokenSource.IsCancellationRequested)
{
cancellationTokenSource.Cancel();
}
cancellationTokenSource = new CancellationTokenSource();
this.Suggestions = await this.searchProvider.SearchAsync(searchCriteria, linkedCts.Token);
Debug.WriteLine("Search({0}) returned {1} records", searchCriteria, results.Count);
}
catch (OperationCanceledException)
{
Debug.WriteLine("Search({0}) cancelled", searchCriteria);
}
}
You cancel your previous query too late, query it as soon as you are able (directly after receiving user input, not inside the next query).
I suppose the problem is multithread access to this.cancellationTokenSource. If say thread 2 cancels thread 1 too late (searchProvider.SearchAsync is completed) this.cancellationTokenSource can be cleared after assignment (i.e. thread 2 executes this.cancellationTokenSource = linkedCts; and thread 1 after that calls this.cancellationTokenSource = null;). This will effectively disable cancelling of thread 2 completely
Thus you'd better have one search cancelled before the next one starts like in the suggestion from #Ned Stoyanov

Categories