I have a legacy scenario where a ref bool
was being used to send a cancellation signal to an implementation. Now, I want to call a Task
-based library method that takes a CancellationToken
instance, which I also want to be cancelled when the boolean changes value.
This is what I have to work with:
void Method(ref bool isCancelled)
{
while (!isCancelled)
{
...
DoThis();
DoThat();
...
}
}
And this is what I want to do:
Task MethodAsync(ref bool isCancelled)
{
while (!isCancelled)
{
...
DoThis();
await DoTheNewThingAsync(isCancelled.ToCancellationToken());
DoThat();
...
}
}
ToCancellationToken()
doesn't exist in this context of course, and is used just to show the intent.
I tried to create a custom implementation of CancellationTokenSource
but there is nothing virtual in the class that I could work with. It's also not possible to create a custom CancellationToken
directly since it is a struct
and cannot be inherited.
I'm aware that using a ref bool
is a poor practice but I can't currently change the underlying implementation that relies on it, so I need a way to use it's value as the cancellation mechanism for the task-based call.
You create a cancellation token by instantiating a CancellationTokenSource object, which manages cancellation tokens retrieved from its CancellationTokenSource. Token property. You then pass the cancellation token to any number of threads, tasks, or operations that should receive notice of cancellation.
CancellationTokenSource - This is the object responsible for creating a cancellation token and sending a cancellation request to all copies of that token. CancellationToken - This is the structure used by listeners to monitor the token's current state.
Canceled state. By throwing an OperationCanceledException and passing it the token on which cancellation was requested. The preferred way to perform is to use the ThrowIfCancellationRequested method.
A CancellationToken enables cooperative cancellation between threads, thread pool work items, or Task objects. In this article, I would like to discuss the mechanism which is applicable for Task objects. When you run a task in C#, it may take a while to execute it.
It's complicated. For a few reasons:
ref
to an async
method. You're using await
, but to use await
, your method needs to be marked async
. And async
methods cannot have ref
parameters. For example, this will not compile:async Task MethodAsync(ref bool isCancelled)
{
while (!isCancelled)
{
DoThis();
await DoTheNewThingAsync(isCancelled.ToCancellationToken());
DoThat();
}
}
That will give you the compiler error:
CS1988: Async methods cannot have ref, in or out parameters
ref
parameters in anonymous methods. I thought about using a Timer
to check the variable. Something like this:public static CancellationToken ToCancellationToken(ref bool isCancelled)
{
var tokenSource = new CancellationTokenSource();
var timer = new System.Timers.Timer()
{
AutoReset = true,
Interval = 100
};
timer.Elapsed += (source, e) =>
{
if (isCancelled)
{
tokenSource.Cancel();
timer.Dispose();
}
};
timer.Enabled = true;
return tokenSource.Token;
}
But that gives you the compiler error:
CS1628: Cannot use ref, out, or in parameter 'isCancelled' inside an anonymous method, lambda expression, query expression, or local function
I don't see any other way to get the bool
into the event handler by reference.
void Method(ref bool isCancelled)
{
while (!isCancelled)
{
DoThis();
using (var tokenSource = new CancellationTokenSource()) {
var mytask = DoTheNewThingAsync(tokenSource.Token);
while (true)
{
//wait for either the task to finish, or 100ms
if (Task.WaitAny(mytask, Task.Delay(100)) == 0)
{
break; //mytask finished
}
if (isCancelled) tokenSource.Cancel();
}
// This will throw an exception if an exception happened in
// DoTheNewThingAsync. Otherwise we'd never know if it
// completed successfully or not.
mytask.GetAwaiter().GetResult();
}
DoThat();
}
}
However, that blocks the caller, so I don't entirely see how that could even be useful (how can the caller change isCancelled
if it's blocked?). But that's kind of what your existing method is doing, so maybe it would work?
But this is super hacky. If you can at all control how anything is done upstream, do that instead.
I've hacked up a somewhat working solution:
public static class TaskRefBoolCancellable
{
public static T SynchronousAwait<T>(Func<CancellationToken, Task<T>> taskToRun, ref bool isCancelled)
{
using (var cts = new CancellationTokenSource())
{
var runningTask = taskToRun(cts.Token);
while (!runningTask.IsCompleted)
{
if (isCancelled)
cts.Cancel();
Thread.Sleep(100);
}
return runningTask.Result;
}
}
}
void Method(ref bool isCancelled)
{
while (!isCancelled)
{
...
DoThis();
var result = TaskRefBoolCancellable.SynchronousAwait(DoTheNewThingAsync, ref isCancelled);
DoThat();
...
}
}
WARNING: This code runs synchronously on calling thread. So there are no guarantees it will work nicely with other parts of the code, as it blocks the calling thread. Also, it polls the isCancelled
variable, making it both ineffective and the cancellation is not immediate.
I would consider this a stop-gap solution as you replace the ref bool isCancelled
with proper task-based cancellation.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With