-1

C# Windows UWP project

I have implemented CancellationTokenSource and CancellationToken in an async method that calls another method. This method contains a while loop which maintains the value of a bool variable as true until the token source is cancelled.

The async method is triggered by a mouse left button pressed event and is cancelled by a mouse left button released event occurring while using a ColorPicker control. The bool variable allows the color values to be sent to a device when true and prevents it when false.

By maintaining the value as true, the device receives varying color values continuously while the pointer is moved around the color picker as long as the mouse button remains down. Once the mouse button is released, the resulting false value (which gets set by the routine that sends the color values to the device) prevents further color messages from being sent to the device.

My code does what I want it too, but I’m concerned about potential side effects that could result if I have not implemented it correctly. I have seen at least one posting on this forum that indicates that the sequence: cancel, dispose and set to null can be used for the CancellationTokenSource. But it concerns me that I have a potentially endless while loop that depends entirely on receiving the cancellation token. So my question is whether disposing of a CancellationTokenSource too soon could prevent token.IsCanellationRequested from being set to true, and if so will adding a delay add any benefit?

The following are the related snippets from my code:

Global variables:

public static bool colorPickerPtrPressed = false;
static CancellationTokenSource cts = null;
static CancellationToken token;

Mouse button events:

private void ColorPicker_PtrPressedEvent(object sender, PointerRoutedEventArgs e)
{
    if(cts == null) cts = new CancellationTokenSource();
    token = cts.Token;

    var picker = sender as ColorPicker.ColorPicker;

    colorPickerPtrPressed = true;
    (picker.DataContext as SpheroViewModel).Color = picker.SelectedColor.Color;

    ColorChange(token);

}

private void ColorPicker_PtrReleasedEvent(object sender, PointerRoutedEventArgs e)
{
    if (cts != null)
    {
        cts.Cancel();
        Task.Delay(500).Wait(); // Allow some time for cancel to take effect
        cts.Dispose();
        cts = null;
    } 
}

Cancellation token methods:

public static async Task ColorChange(CancellationToken token)
{
    await Task.Run(() =>
    AllowColorChange(token), token);
}

public static void AllowColorChange(CancellationToken token)
{
        while (!token.IsCancellationRequested)
        {
            colorPickerPtrPressed = true; // Maintain value as true
            Task.Delay(100).Wait(); // allow color updates periodically
        }
    return;
}

3 Answers3

3

So my question is whether having a delay between cancelling and disposing a CancellationTokenSource, as I have done in "ColorPicker_PtrReleasedEvent" below, will provides any assurance that the while loop will terminate?

No. The cancellation model is cooperative and calling Cancel simply provides a notification of cancellation by setting the IsCancellationRequested property of all tokens to true.

It's then up to any cancellable API, i.e. any method that accepts a CancellationToken, to monitor the value of this property and respond to the cancellation request.

So ColorPicker_PtrReleasedEvent cannot assure that the while loop will terminate in AllowColorChange.

mm8
  • 163,881
  • 10
  • 57
  • 88
  • Are you saying that cts.Cancel() will always set token.IsCancellationRequested to true (which the while loop monitors) and that a delay afterwards adds nothing? – Stroker347 Feb 02 '21 at 22:10
  • 1
    @Stroker347 if you want to ensure that the cancelable operation was been indeed completed in a canceled state, there is no substitute to waiting or (preferably) awaiting that operation after you `Cancel` the cancellation source. Using arbitrary delays is a recipe for bad performing and sporadically failing applications. – Theodor Zoulias Feb 02 '21 at 22:53
  • Do you mean "await cts.Cancel()" or "await AllowColorChange(token)"? – Stroker347 Feb 02 '21 at 23:08
  • @Stroker347 `await AllowColorChange(token)`. Or even better `await _task`, where `_task` is the cached result of invoking the `AllowColorChange(token)` earlier. Which should be an asynchronous method that returns a `Task`, and have the `Async` suffix according to the [guidelines](https://learn.microsoft.com/en-us/dotnet/standard/asynchronous-programming-patterns/task-based-asynchronous-pattern-tap#naming-parameters-and-return-types). And a better name in general. `AllowColorChange` is a name suitable for a `bool` property, not a method! – Theodor Zoulias Feb 02 '21 at 23:15
  • Actually, I would know if the while loop didn't terminate because the color changes would continue to be sent to my device after the mouse button was released. However, if I awaited it and checked for completion, I could then reissue the Cancel() command if it didn't get canceled upon release of the mouse button. Testing has demonstrated that the while loop is terminating as desired even without any delay so maybe I'm worrying over nothing. – Stroker347 Feb 03 '21 at 00:09
  • @Stroker347: Yes, that's basically what I am saying. – mm8 Feb 03 '21 at 19:39
0

As recommended by TZ I modified the last three methods to await a canceled flag from the AllowColorChange(token) method and then used that result as a permissive to dispose() of the CancellationTokenResult and set it to null. If anyone sees a problem with what I have done, please let me know. The following is the modified code, which appears to work well:

    private void ColorPicker_PntrReleasedEvent(object sender, PointerRoutedEventArgs e)
    {
        if (cts != null)
        {
            cts.Cancel();
            //Task.Delay(200).Wait(); // Allow some time for cancel to take effect
            //cts.Dispose();
            //cts = null;
        }
    }

    public static async Task ColorChange(CancellationToken token)
    {
        bool task = false;
        task = await Task.Run<bool>(() =>
        AllowColorChange(token), token);
        if (task)
        {
            cts.Dispose();
            cts = null;
        }
        else
        {
            // Shouldn't ever reach this point
            bool isBrkPnt = true;
        }
    }

    public static async Task<bool> AllowColorChange(CancellationToken token)
    {
            while (!token.IsCancellationRequested)
            {
                colorPickerPtrPressed = true; // Maintain value as true
                await Task.Delay(100); // allow color updates periodically
            }
        return true; // signal that task was canceled
    }

}
  • Are you sure that the commands `cts.Dispose(); cts = null;` will dispose the canceled `cts`, and not a new `cts` created meanwhile by the `ColorPicker_PtrPressedEvent` handler? Besides that, you don't pass the `token` to the `Task.Delay` method, so the cancellation will be slightly postponed (by ~50 msec on average). Also the standard practice in case of cancellation is to propagate an `OperationCanceledException`, not a `bool` value. This means that the `IsCancellationRequested` property should be rarely used, and the `ThrowIfCancellationRequested` method should be used instead. – Theodor Zoulias Feb 03 '21 at 03:02
  • 1
    I only create a new CancellationTokenSource if the current one is null, so there shouldn't ever be more than one at a time so I think I'm OK on the first point. I tried passing the token to the Delay method and it caused my while loop to stop functioning the way I want so I'll have to investigate this further. A bool value was a natural choice for how I am using the return value but I'll look into what you've suggested as a better choice. Thanks for your suggestions, they have been very helpful and I'm no longer worried that I may be prematurely disposing of the CancellationTokenSource. – Stroker347 Feb 03 '21 at 05:16
  • TZ, I modified the body of Task ColorChange(token) to: "try{ await Task.Run(async() => {while(true){token.ThrowIfCancellationRequested(); colorPickerPtrPressed = true; await Task.Delay(100, token);},token}; } catch (OperationCanceledException ex) when (ex.CancellationToken == token){cts.Dispose(); cts = null;}" which works and eliminates the use of the AllowColorChange method. Also, passing the token to the Delay method now works. Are these changes more along the lines of what you suggested? – Stroker347 Feb 03 '21 at 15:07
  • Yeap, that's much better. The reason that you need to set `colorPickerPtrPressed = true` every 100 msec is unclear to me, and I guess that it's not the optimal solution for whatever you are trying to do, but it's probably good enough. :-) – Theodor Zoulias Feb 03 '21 at 15:14
  • 1
    Each time I send my device a color command I set colorPickerPtrPressed = false, in the method that sends the command, to prevent further changes as I move the pointer out of the color picker control. But I also wanted to have the ability to hold the mouse button down and send a continuous stream until I get just the right color. I just don't need or want too overload my device with color change commands when a periodic update is adequate. Thanks again for your help, it's been very enlightening. – Stroker347 Feb 03 '21 at 19:11
0

After implementing recommendations suggested by Theodor Zoulias, the final code is now as shown below. It can be seen that no arbitrary delay is used between canceling and disposing the CancellationTokenSource, but rather the disposal is moved to a catch{} block that is triggered by an OperationCanceledException resulting from throwing token.ThrowIfCancellationRequested(); from within the while() loop, which was moved inside a try{} block and it's test parameter set to true. It is no longer necessary to test for the value of token.IsCancellationRequested as a parameter of the while loop. This coding ensures that the disposal will not occur until the task within the try{} block is canceled.

     private void ColorPicker_PntrPressedEvent(object sender, PointerRoutedEventArgs e)
    {
        if(cts == null) cts = new CancellationTokenSource();
        token = cts.Token;

        var picker = sender as ColorPicker.ColorPicker;

        colorPickerPtrPressed = true; // True allows new values of color to be sent to device
        (picker.DataContext as SpheroViewModel).Color = picker.SelectedColor.Color;

        ColorChange(token); // Don't await this

    }

    private void ColorPicker_PntrReleasedEvent(object sender, PointerRoutedEventArgs e)
    {
        if (cts != null)
        {
            cts.Cancel();
        }
    }

    public static async Task ColorChange(CancellationToken token)
    {
        try
        {
            await Task.Run(async () =>
            {
                while (true)
                {
                    token.ThrowIfCancellationRequested();
                    colorPickerPtrPressed = true; // Maintain value as true while mouse button remains pressed
                    await Task.Delay(100, token); // allow color updates periodically
                }
            }, token);
        }
        catch (OperationCanceledException ex) when (ex.CancellationToken == token) // includes TaskCanceledException
        {
            if (cts != null) // Shouldn't arrive here if it is null but check just in case
            {
                try
                {
                    cts.Dispose();
                    cts = null;
                }
                catch (ObjectDisposedException e)
                {
                    // Already disposed, do nothing
                    bool brkPnt = true;
                }
            }
        }
    }

}

}

  • Hmm, wrapping the loop in a `Task.Run` complicates matters, because now not everything is going to happen on the same thread (the UI thread). It is now possible that the `cts.Cancel();` will be invoked on the UI thread just after the `cts.Dispose` has been invoked on the background thread, resulting to an `ObjectDisposedException`. I would suggest to remove `Task.Run`, since it doesn't seem to offer anything. Invoking a continuation on the UI thread every 100 msec that only updates a `bool` variable, is nothing to worry about. – Theodor Zoulias Feb 03 '21 at 21:46
  • Btw have you considered replacing this machinery with a simple `System.Windows.Forms.Timer`? You could toggle its `Enabled` property (or invoke its `Start`/`Stop` methods) at the appropriate moments. – Theodor Zoulias Feb 03 '21 at 21:52
  • OK. Everything appears to work as well without the Task.Run wrapper as it did with it. – Stroker347 Feb 03 '21 at 21:57
  • 1
    TZ, just FYI: after further study I found that, with the Task.Run wrapper present, the creation, cancellation and disposal of a CancellationTokenSource all take place in the UI thread. Only the while() loop runs in a threadpool thread. I like this configuration because the "await Task.Delay()" in the while() loop is optional and if it is not present the while() loop will block all further execution unless the "await Task.Run" wrapper is present. I believe the final version of the code that I posted is good. My intent was to implement an async method and I believe with your help I succeeded. – Stroker347 Feb 05 '21 at 23:26