How to cancel an async connect method after X seconds in C# - c#

I am creating a mqtt client in C# with the libray MQTTNet.
I wan't my client to connect to a broker and stop after 1 second if doesn't succeed.
Here is the function I made below.
private async Task TryConnect(MqttClientOptions options)
{
CancellationTokenSource tokenSource = new CancellationTokenSource();
mqttClient!.ConnectAsync(options, tokenSource.Token);
await Task.Delay(1000);
tokenSource.Cancel();
}
The method is working but it gives me a warning when I call the method ConnectAsync because I am not using an await operator before the call. And if I use the await operator the method will continue until it will raise an error.
Is there a way to do this without warnings ? Because even if it is working I have the feeling that this is not the better way to do it and that there is a cleaner way.
Thank you for your help,
Emmanuel

You should probably use CancelAfter:
CancellationTokenSource tokenSource = new CancellationTokenSource();
tokenSource.CancelAfter(TimeSpan.FromSeconds(1));
await mqttClient!.ConnectAsync(options, tokenSource.Token);
An alternative would be to store the task from connectAsync and await if after the cancel call.
Note that in either case you are not guaranteed that the connection will actually cancel, it entirely depends on the ConnectAsync-implementation. In some cases it might be approrpiate to use Task.WhenAny to await either the connection, or a Task.Delay, i.e. you do no longer care about the connection after the timeout. You should probably also catch OperationCancelledException, since that is the standard method to communicate that the operation actually was cancelled.

You can specify a timeout for a cancellation token:
var tokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(1));
If you're cancelling it like this you will need to catch a TaskCanceledException, but you can ignore the cancellation if you want.
private async Task TryConnect(MqttClientOptions options)
{
var tokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(1));
try
{
mqttClient!.ConnectAsync(options, tokenSource.Token);
}
catch (TaskCanceledException)
{
// Do nothing.
}
}
Alternatively you could return a bool to indicate whether the connection was successful:
private async Task<bool> TryConnect(MqttClientOptions options)
{
var tokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(1));
try
{
mqttClient!.ConnectAsync(options, tokenSource.Token);
}
catch (TaskCanceledException)
{
return false;
}
return true;
}

Related

CancellationTokenSource for changing state after few seconds

I'm trying to recreate the effect one sees in Outlook / Gmail: a few seconds after opening an unread email, it is marked as read. But if one clicks a different email before that time elapses then the email remains unread.
private CancellationTokenSource? _cts;
private async Task OnEmailClicked(int id)
{
if (_cts != null)
{
_cts.Cancel(); // I suspect problem is here
_cts.Token.ThrowIfCancellationRequested(); // or here
_cts.Dispose();
_cts = null;
}
await LoadEmail(id);
try
{
_cts = new CancellationTokenSource();
await Task.Delay(3000, _cts.Token);
}
catch (TaskCanceledException)
{
return;
}
finally {
_cts.Dispose();
_cts = null;
}
await MarkEmailAsRead(id);
}
That gives me weird results. Sometimes it works and sometimes not. I'm unsure where/how to create, cancel and dispose the token source. Obviously my code is wrong.
What is the correct way to achieve this?
(I don't need to "fix" this code - I'm happy to throw it away and do it the proper way, if you can show me how. I've only included it to demonstrate what I've tried.)
First, calling ThrowIfCancellationRequested() will break things. It will throw an exception every time that line is hit (because you just cancelled the token the line before) and the rest of the method won't run.
The other issue I see is that you're setting _cts to null after cancelling it. Remember that you will have two versions of this method in progress at the same time. Assuming both versions of the method will be running on the UI thread then this entire block of code will run uninterrupted (I've removed ThrowIfCancellationRequested()):
if (_cts != null)
{
_cts.Cancel();
_cts.Dispose();
_cts = null;
}
And then once LoadEmail(id) is awaited, then the previous incomplete version of the method is given a chance to run and it goes into the catch and the finally where it calls _cts.Dispose(). But _cts has already been set to null so you will get a NullReferenceException and MarkEmailAsRead(id) will never get run.
So I would only call Dispose() in one place, not two. But I would also keep a reference to the CancellationTokenSource that we created so that if something new is assigned to _cts, we can still call Dispose() on the one we created. That would look something like this:
private CancellationTokenSource? _cts;
private async Task OnEmailClicked(int id)
{
_cts?.Cancel();
// Keep a local reference to the token source so that we can still call Dispose()
// on the object we created here even if _cts gets assigned a new object by another
// run of this method
var cts = _cts = new CancellationTokenSource();
await LoadEmail(id);
try
{
await Task.Delay(3000, cts.Token);
}
catch (TaskCanceledException)
{
return;
}
finally {
// Only assign _cts = null if another run hasn't already assigned
// a new token
if (cts == _cts) _cts = null;
cts.Dispose();
}
await MarkEmailAsRead(id);
}
I didn't test this, but see if it fulfills your requirements.
If the use of cts vs _cts looks confusing to you, it may help to read up on reference types. But the important part is that assigning a new instance to _cts doesn't destroy the object that it used to refer to, and any other part of the code that still has a reference to that object can still use it.
If you've never seen the ?. before, it's the null-conditional operator.
As I understand it, you're essentially restarting a watchdog timer for three seconds every time you click a different email. If the WDT expires without getting a new click then you go ahead and mark the latest email as 'Read'.
For years, I used (successfully) this same approach to cancel the old task when starting a new one for the WDT. But then I realized I could just do this:
int _wdtCount = 0;
// Returning 'void' to emphasize that this should 'not' be awaited!
async void onEmailClicked(int id)
{
_wdtCount++;
var capturedCount = _wdtCount;
await Task.Delay(TimeSpan.FromSeconds(3));
// If the 'captured' localCount has not changed after waiting 3 seconds
// it indicates that no new selections have been made in that time.
if(capturedCount.Equals(_wdtCount))
{
await MarkEmailAsRead(id);
}
}
async Task MarkEmailAsRead(int id)
{
Console.WriteLine($"The email with the ID '{id}' has been marked read");
}
Anyway, works for me and it's just a thought...
TESTBENCH
#region T E S T
onEmailClicked(id: 1);
await Task.Delay(TimeSpan.FromSeconds(1));
onEmailClicked(id: 2);
await Task.Delay(TimeSpan.FromSeconds(1));
onEmailClicked(id: 3);
await Task.Delay(TimeSpan.FromSeconds(4));
onEmailClicked(id: 10);
await Task.Delay(TimeSpan.FromSeconds(2));
onEmailClicked(id: 20);
await Task.Delay(TimeSpan.FromSeconds(1));
onEmailClicked(id: 30);
await Task.Delay(TimeSpan.FromSeconds(4));
#endregion T E S T
EDIT
Though I realize that the post no longer has the wording entertaining 'other' options than task cancellation, I still wanted to incorporate the excellent comments and improve my original answer by putting the WatchDogTimer in a class...
class WatchDogTimer
{
int _wdtCount = 0;
public TimeSpan Interval { get; set; } = TimeSpan.FromSeconds(1);
public void ThrowBone(Action action)
{
_wdtCount++;
var capturedCount = _wdtCount;
Task
.Delay(Interval)
.GetAwaiter()
.OnCompleted(() =>
{
// If the 'captured' localCount has not changed after
// awaiting the Interval, it indicates that no new
// 'bones' have been thrown during that interval.
if (capturedCount.Equals(_wdtCount))
{
action();
}
});
}
}
... and eliminating the async void from the posit.
void onEmailClicked(int id)
=> _watchDog.ThrowBone(() => MarkEmailAsRead(id));
void MarkEmailAsRead(int id)
{
// BeginInvoke(()=>
// {
Console.WriteLine($"Expired: {_stopWatch.Elapsed}");
Console.WriteLine($"The email with the ID '{id}' has been marked read");
// });
}
TESTBENCH
#region T E S T
// Same test parameters as before.
System.Diagnostics.Stopwatch _stopWatch =
new System.Diagnostics.Stopwatch();
WatchDogTimer _watchDog =
new WatchDogTimer { Interval = TimeSpan.FromSeconds(3) };
_stopWatch.Start();
Console.WriteLine(_stopWatch.Elapsed);
onEmailClicked(id: 1);
await Task.Delay(TimeSpan.FromSeconds(1));
Console.WriteLine(_stopWatch.Elapsed);
onEmailClicked(id: 2);
await Task.Delay(TimeSpan.FromSeconds(1));
Console.WriteLine(_stopWatch.Elapsed);
onEmailClicked(id: 3);
await Task.Delay(TimeSpan.FromSeconds(4));
Console.WriteLine(_stopWatch.Elapsed);
onEmailClicked(id: 10);
await Task.Delay(TimeSpan.FromSeconds(2));
Console.WriteLine(_stopWatch.Elapsed);
onEmailClicked(id: 20);
await Task.Delay(TimeSpan.FromSeconds(1));
Console.WriteLine(_stopWatch.Elapsed);
onEmailClicked(id: 30);
await Task.Delay(TimeSpan.FromSeconds(4));
#endregion T E S T
First, I am assuming that your code is single-threaded. The CancellationTokenSource.Dispose method is not thread-safe, so trying to dispose CancellationTokenSources that are shared in a multithreaded environment is a small nightmare. You could check that your code is single-threaded, by searching for Task.Run and .ConfigureAwait(false) in the code that interacts with the CancellationTokenSources. If you find any, you might be in trouble.
Second, you should be aware that the CancellationTokenSource.Cancel method is "jumpy". It immediately invokes all the callbacks that have been registred previously via the CancellationToken.Register route. Code like await Task.Delay(3000, _cts.Token); implicitly registers a callback, so it is possible that the execution flow will jump to the code after the await before continuing with the code after the Cancel. Your code should be prepared for such jumps. You shouldn't assume that the Cancel will complete quickly, nor that it won't throw. In case a registered callback fails, the exception will be surfaced by the Cancel call.
As for the specifics of your code, your mistake is that you interact with the _cts field too much. You should reduce the interactions to the minimum. You should also assume that after an await the _cts will not store the same instance that you assigned before the await. Your code is single-threaded, but asynchronous. Asynchronous code has a complex execution flow. I agree with Gabriel Luci that you should Dispose in one place only. Using async void is not that bad, since it's normal for UI event handlers to be async void. It might bite you when you are done with the application and attempt to close the Window, because you will have no way to wait for the completion of the pending async operations before closing. So you might get annoying errors after closing the Window, because the async operations will try to interact with UI components that have been disposed. Ideally you should store a reference of the latest asynchronous operation (the Task), and await it before starting the next operation, or before closing the Window. But for the purpose of keeping the answer simple I will omit this complexity. Here is my suggestion:
private CancellationTokenSource _cts;
private async void OnEmailClicked(int id)
{
_cts?.Cancel();
CancellationTokenSource cts = new();
try
{
_cts = cts;
await LoadEmail(id);
try { await Task.Delay(3000, cts.Token); }
catch (OperationCanceledException) { return; } // Do nothing
}
finally
{
if (cts == _cts) _cts = null;
cts.Dispose();
}
await MarkEmailAsRead(id);
}

Cancellation doesn't go as expected

The while loop currently blocks the thread as it retries forever until the connection is established. I expect it to retry forever but it shouldn't block the thread (just like if we call StartAsync without awaiting it) and make it possible for us to call StopAsync during StartAsync's execution and cancel that process of retrying forever. It would require passing a CancellationToken to ConnectAsync too.
await client.StartAsync(); // do not block
await Task.Delay(5000);
await client.StopAsync();
I was thinking of moving the CancellationToken before the while loop and pass it to the while loop as well as loop until while (!await ConnectAsync().ConfigureAwait(false) && !_tokenSource.IsCancellationRequested) and then wrap that logic into a Task.Run to prevent blocking. What do you think?
Full code (#GeneralClient2 class)
public async Task StartAsync()
{
// Prevent a race condition
await _semaphore.WaitAsync().ConfigureAwait(false);
try
{
if (IsRunning)
{
return;
}
while (!await ConnectAsync().ConfigureAwait(false))
{
}
IsRunning = true;
Debug.Assert(_clientWebSocket != null);
_tokenSource = new CancellationTokenSource();
_processingSend = ProcessSendAsync(_clientWebSocket, _tokenSource.Token);
_processingData = ProcessDataAsync(_tokenSource.Token);
_processingReceive = ProcessReceiveAsync(_clientWebSocket);
}
finally
{
_semaphore.Release();
}
}
public async Task StopAsync()
{
if (!IsRunning)
{
return;
}
_logger.LogDebug("Stopping");
try
{
if (_clientWebSocket is { State: not (WebSocketState.Aborted or WebSocketState.Closed or WebSocketState.CloseSent) })
{
await _clientWebSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None).ConfigureAwait(false);
}
}
catch
{
}
await _processingReceive.ConfigureAwait(false);
_logger.LogDebug("Stopped");
}
private async ValueTask<bool> ConnectAsync()
{
_logger.LogDebug("Connecting");
var ws = new ClientWebSocket();
try
{
await ws.ConnectAsync(new Uri(_url), CancellationToken.None).ConfigureAwait(false);
Connected?.Invoke(this, EventArgs.Empty);
}
catch (Exception) // WebSocketException or TaskCanceledException
{
ws.Dispose();
return false;
}
_clientWebSocket = ws;
_logger.LogDebug("Connected");
return true;
}
The while loop currently blocks the thread
Are you sure? because ClientWebSocket.ConnectAsync explicitly states
This operation will not block. The returned Task object will complete
after the connect request on the ClientWebSocket instance has
completed.
So ConnectAsync should not block, and therefore neither the while loop. But even if it is non-blocking it might still consume significant CPU usage. Or something might throw before you even call ClientWebSocket.ConnectAsync, and since you just eat all the exceptions you will never know.
make it possible for us to call StopAsync during StartAsync's execution and cancel that process of retrying forever
You should be careful when stopping some service in a threaded or async context. Since some other task might find that the needed resource have been disposed.
while (!await ConnectAsync().ConfigureAwait(false) && !_tokenSource.IsCancellationRequested)
The problem with this is that the loop will exit and continue running the method if no connection has been established. Because of this it is recommended to use ThrowIfCancellationRequested, even if it is somewhat painful to use exceptions for flow control.
My recommendations would be to make StartAsync take a cancellationToken that aborts the connection process. This method should return an object representing the connection, i.e. Task<MyConnection>. This connection object should be disposable. Stopping the connection could be done like
// Create connection
var cts = new CancellationTokenSource();
var myStartAsyncConnectionTask = StartAsync(cts.Token);
// Close connection
cts.Cancel();
try{
var myConnection = await myStartAsyncConnectionTask;
myConnection.Dispose();
}
catch(OperationCancelledException)
{
...
}
That should work regardless of what state the connection is in. If a connection has been established the cancellation will do nothing, and the object will be disposed. If it has failed to connect, awaiting the task should throw. Note that StartAsync need to be written so that it cleans up any created resource in case the method throws any exception at any stage.

GetContextAsync() with Cancellation Support

So I'm spinning up a HttpListener to wait for an OAuth2 response. In an ideal world, this is only going to be alive for a few seconds while the user logs in in the browser and we get posted the token.
I'd also like for this to have a CancellationToken so that the user can stop listening after a delay should they so wish.
My initial idea was to use something along the lines of:
_listener.Start();
Task<HttpListenerContext> t = _listener.GetContextAsync();
while (!cancelled.IsCancellationRequested)
{
if (t.IsCompleted)
{
break;
}
await Task.Run(() => Thread.Sleep(100));
}
HttpListenerContext ctx = t.Result;
//...
_listener.Stop();
But that doesn't sit right with me for so many reasons (weird async usage, polling, etc.).
So then I thought I might be able to use the synchronous version _listener.GetContext() in conjunction with Task.Run(func<T>, CancellationToken):
_listener.Start()
HttpListenerContext ctx = await Task.Run(() => _listener.GetContext(), cancelled);
//...
_listener.Stop();
This is a little better, the code's at least tidier, although it seems hacky using a synchronous version of the method asynchronously with a Task...
However this doesn't behave how I'd expect (aborting the running task when the token is cancelled).
This strikes me as something that really ought to be fairly simple to do so I assume I'm missing something.
So my question is thus... How do I listen asynchronously with a HttpListener in a cancellable fashion?
Because the GetContextAsync method does not support cancellation, it basically means that it is unlikely you can cancel the actual IO operation, yet unlikely to cancel the Task returned by the method, until you Abort or Stop the HttpListener. So the main focus here is always a hack that returns the control flow to your code.
While both the answers from #guru-stron and #peter-csala should do the trick, I just wanted to share another way without having to use Task.WhenAny.
You could wrap the task with a TaskCompletionSource like this:
public static class TaskExtensions
{
public static Task<T> AsCancellable<T>(this Task<T> task, CancellationToken token)
{
if (!token.CanBeCanceled)
{
return task;
}
var tcs = new TaskCompletionSource<T>();
// This cancels the returned task:
// 1. If the token has been canceled, it cancels the TCS straightaway
// 2. Otherwise, it attempts to cancel the TCS whenever
// the token indicates cancelled
token.Register(() => tcs.TrySetCanceled(token),
useSynchronizationContext: false);
task.ContinueWith(t =>
{
// Complete the TCS per task status
// If the TCS has been cancelled, this continuation does nothing
if (task.IsCanceled)
{
tcs.TrySetCanceled();
}
else if (task.IsFaulted)
{
tcs.TrySetException(t.Exception);
}
else
{
tcs.TrySetResult(t.Result);
}
},
CancellationToken.None,
TaskContinuationOptions.ExecuteSynchronously,
TaskScheduler.Default);
return tcs.Task;
}
}
And flow the control like this:
var cts = new CancellationTokenSource();
cts.CancelAfter(3000);
try
{
var context = await listener.GetContextAsync().AsCancellable(cts.Token);
}
catch (TaskCanceledException)
{
// ...
}
I would suggest creating cancelable infinite task (Task.Delay(Timeout.Infinite, token) for example) and use Task.WhenAny. Something like that:
var cts = new CancellationTokenSource(); // token source controled by consumer "outside"
var token = cts.Token;
var httpListener = new HttpListener();
httpListener.Start();
var t = httpListener.GetContextAsync();
// to cancel the infinite delay task if listener finishes first
var localCts = CancellationTokenSource.CreateLinkedTokenSource(token);
var completed = await Task.WhenAny(t, Task.Delay(Timeout.Infinite, localCts.Token));
if (completed == t) // check that completed task is one from listener
{
localCts.Cancel(); // cancel the infinite task
HttpListenerContext ctx = t.Result;
//...
}
httpListener.Stop();
Here is yet another solution:
var cancellationSignal = new TaskCompletionSource<object>();
var contextTask = _listener.GetContextAsync();
using (cancelled.Register(state => ((TaskCompletionSource<object>)state).TrySetResult(null), cancellationSignal))
{
if (contextTask != await Task.WhenAny(contextTask, cancellationSignal.Task).ConfigureAwait(false))
break; //task is cancelled
}
Because we can't await the CancellationToken that's why have to apply the following trick
The CancellationToken does expose a Register method, where we can define a callback which will be called whenever the cancellation occurs
Here we can provide a delegate which sets an awaitable to completed
So, we can await that task
In order to create a Task which is set to completed whenever the cancellation occurs I've used TaskCompletionSource. You could also use SemaphoreSlim or any other signalling object which has async wait, like AsyncManualResetEvent.
So, we pass the cancellationSignal to the Register as a state parameter
Inside the delegate we have to cast it back to TCS to be able to call the TrySetResult on it
Inside the using block we await a Task.WhenAny
It will return that Task which finishes first
If that Task is the cancellation then we can break / return / throw ...
If that Task is the contextTask then we can continue the normal flow

Timeout and stop a Task

I have a console app,
{
StartThread();
//must be true, windows system wants to know it is started
return true;
}
I'm trying to create a safety timeout function for this Task. But the task keeps running...
The method DoSomething calls other async methods and awaits their result
Do anyone have an idea why my task don't stop? Maybe a good code example on what to do
public async void StartThread()
{
var tokenSource = new CancellationTokenSource();
var token = tokenSource.Token;
try
{
var timeout = 1000;
Task task = new Task(() => DoSomething(token), token);
task.Start();
if (await Task.WhenAny(task, Task.Delay(timeout, token)) == task)
{
if (token.IsCancellationRequested)
task.Dispose();
await task;
if (task.IsCompleted)
{
task.Dispose();
tokenSource.Cancel();
tokenSource.Dispose();
}
else
{
log.WriteToFile("Timeout_ ");
}
}
else
tokenSource.Cancel();
}
catch (Exception e)
{
Console.WriteLine("--StartThread ...there is an exception----");
}
finally
{
Thread.Sleep(300000); // 5 minutter
StartThread();
}
}
While not create CancellationTokenSource from given timeout?
var timeout = 1000;
//DONE: don't forget to dispose CancellationTokenSource instance
using (var tokenSource = new CancellationTokenSource(timeout)) {
try {
var token = tokenSource.Token;
//TODO: May be you'll want to add .ConfigureAwait(false);
Task task = Task.Run(() => DoSomething(token), token);
await task;
// Completed
}
catch (TaskCanceledException) {
// Cancelled due to timeout
log.WriteToFile("Timeout_ ");
}
catch (Exception e) {
// Failed to complete due to e exception
Console.WriteLine("--StartThread ...there is an exception----");
//DONE: let's be nice and don't swallow the exception
throw;
}
}
You should hardly ever Dispose a Task, since iot is managed by C# internals and it is taken care of. Also, you Dispose way too eagerly, for example:
if (token.IsCancellationRequested)
task.Dispose();
await task;
I do not think so you want still await task if it cancelled and disposed. I guess it will not work at all.
Also if you use async, do not mix blocking calls such as Thread.Sleep - that can lead to disaster...
After all, you use cancellation token with some delay task to imitate a timeout - that's OK, but why do put unnecessary code, when you have great API at hand. Just make use of special contructor of CancellationTokenSource:
public CancellationTokenSource (int millisecondsDelay);
Here's the docs
After a timeout you are setting the CancellationToken and then immediately sleeping the thread for 5 minutes. Thus DoSomething() never gets a chance to continue running and react to the token being cancelled.

How to safely cancel a task using a CancellationToken and await Task.WhenAll

I have a framework which creates a CancellationTokenSource, configures CancelAfter, then calls an async method and passes the Token. The async method then spawns many tasks, passing the cancellation token to each of them, and then awaits the collection of tasks. These tasks each contain logic to gracefully cancel by polling IsCancellationRequested.
My issue is that if I pass the CancellationToken into Task.Run() an AggregateException is thrown containing a TaskCanceledException. This prevents the tasks from gracefully canceling.
To get around this I can not pass the CancelationToken into Task.Run, however I'm not sure what I will be losing. For instance, I like the idea that if my task hangs and cannot perform the graceful cancel this exception will force it down. I was thinking I could string along two CancelationTokens to handle this, one 'graceful' and the other 'force'. However, I don't like that solution.
Here is some psudo-code representing what I described above..
public async Task Main()
{
CancellationTokenSource cts = new CancellationTokenSource();
cts.CancelAfter(30000);
await this.Run(cts.Token);
}
public async Task Run(CancellationToken cancelationToken)
{
HashSet<Task> tasks = new HashSet<Task>();
foreach (var work in this.GetWorkNotPictured)
{
// Here is where I could pass the Token,
// however If I do I cannot cancel gracefully
// My dilemma here is by not passing I lose the ability to force
// down the thread (via exception) if
// it's hung for whatever reason
tasks.Add(Task.Run(() => this.DoWork(work, cancelationToken))
}
await Task.WhenAll(tasks);
// Clean up regardless of if we canceled
this.CleanUpAfterWork();
// It is now safe to throw as we have gracefully canceled
cancelationToken.ThrowIfCancellationRequested();
}
public static void DoWork(work, cancelationToken)
{
while (work.IsWorking)
{
if (cancelationToken.IsCancellationRequested)
return // cancel gracefully
work.DoNextWork();
}
}
I recommend that you follow the standard cancellation pattern of throwing an exception rather than just returning:
public static void DoWork(work, cancellationToken)
{
while (work.IsWorking)
{
cancellationToken.ThrowIfCancellationRequested();
work.DoNextWork();
}
}
If you have cleanup work to do, that's what finally is for (or using, if you can refactor that way):
public async Task Run(CancellationToken cancellationToken)
{
HashSet<Task> tasks = new HashSet<Task>();
foreach (var work in this.GetWorkNotPictured)
{
tasks.Add(Task.Run(() => this.DoWork(work, cancellationToken))
}
try
{
await Task.WhenAll(tasks);
}
finally
{
this.CleanUpAfterWork();
}
}
Provide the CancellationToken to Task.Run in addition to passing it to the method doing the work. When you do this Task.Run can see that the exception thrown was caused by the CancellationToken it was given, and will mark the Task as cancelled.
tasks.Add(Task.Run(() => this.DoWork(work, cancelationToken),
cancelationToken));
Once you've done this you can ensure that DoWork throws when the token is cancelled, rather than checking IsCancellationRequested to try to end by being marked as "completed successfully".

Categories

Resources