Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it possible to convert a `ref bool` to a CancellationToken?

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.

like image 648
julealgon Avatar asked Nov 01 '19 14:11

julealgon


People also ask

How do I create a CancellationToken?

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.

What is difference between CancellationTokenSource and CancellationToken?

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.

Which method can you use to cancel an ongoing operation that uses CancellationToken?

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.

Why do we use cancellation token in C#?

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.


2 Answers

It's complicated. For a few reasons:

  1. You cannot pass a parameter by 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

  1. You cannot use 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.

  1. The closest I could get is something like this:
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.

like image 181
Gabriel Luci Avatar answered Oct 05 '22 22:10

Gabriel Luci


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.

like image 27
Euphoric Avatar answered Oct 05 '22 23:10

Euphoric