Search code examples
c#asynchronouswebsocketasync-awaitcancellation-token

Task cancellation in C# not working as expected - need to clear old tasks before starting new ones


I have a C# application where I manage multiple IP cameras. Each camera runs a task that performs a series of operations like starting a socket, streaming data, and running an object detection model. I manage these tasks using a dictionary that holds the camera index and its corresponding CancellationTokenSource.

My issue is that when I change the settings for a camera, I want to first cancel the existing task for that camera and then start a new one. However, it seems like the new task starts before the old one finishes clearing, and this creates problems.

Here's the sample code:

Main method that starts the task for a camera:

internal static Dictionary<int, Tuple<int, CancellationTokenSource>> tokenSources = new Dictionary<int, Tuple<int, CancellationTokenSource>>();

private async Task IPCameraMethod()
{
    if (string.IsNullOrWhiteSpace(textBoxUrl.Text) ||
        !int.TryParse(SourceComboBox.Name.Replace("comboBox", ""), out int cameraIndex) ||
        comboBoxSavedCameras.SelectedIndex < 0)
    {
        return;
    }

    string url = textBoxUrl.Text;
    int selectedItemIndex = comboBoxSavedCameras.SelectedIndex;

    if (tokenSources.TryGetValue(cameraIndex, out var tuple))
    {
        if (selectedItemIndex != tuple.Item1)
        {
            tuple.Item2.Cancel();
            tuple.Item2.Dispose();
            tokenSources.Remove(cameraIndex);
        }
    }

    var newCts = new CancellationTokenSource();
    tokenSources[cameraIndex] = Tuple.Create(selectedItemIndex, newCts);

    Debug.WriteLine("CREATING NEW TASK");
    await Task.Factory.StartNew(() =>
        Camera.IPCameraService.Main(SourceImageControl, selectedItemIndex, newCts.Token, url),
        newCts.Token,
        TaskCreationOptions.DenyChildAttach,
        TaskScheduler.Default
    );
}

Task method being called

public async Task Main(System.Windows.Controls.Image imageControl, int cameraIndex, CancellationToken token, string cameraUrl)
{
    CameraURLs[cameraIndex] = cameraUrl;

    await StartSocket(cameraIndex);
    await StartStream(cameraIndex, cameraUrl);

    EventHandler(cameraUrl, cameraIndex);
    while (!token.IsCancellationRequested)
    {
        var frame = await RunYolo(cameraIndex);
        if (frame != null)
            await UpdateDisplay(frame, imageControl);
    }

    if (token.IsCancellationRequested)
    {
        Debug.WriteLine("Clearing Websocket!");
        var ws = CameraWebsockets[cameraIndex];
        if (ws != null && ws.State == System.Net.WebSockets.WebSocketState.Open)
        {
            await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "Cancellation requested", CancellationToken.None);
            await Task.Delay(500);
        }
    }
}

Issue: The output I see is:

CREATING NEW TASK
Clearing Websocket!

I would expect to see "Clearing Websocket!" before "CREATING NEW TASK". How can I enforce this order?

Attempts:

I tried to wait for the task to complete using await but I'm still experiencing the issue.

Any help would be greatly appreciated!


Solution

  • There's a couple of problems. First, your code is using Task.Factory.StartNew for asynchronous code, which is wrong ~99.9% of the time. I am honestly surprised at how often it's used, since it's just wrong; I see it quite often. The correct replacement is Task.Run.

    Second, your code cancels the CTS but then it doesn't await the old task before starting the next one. Your code should save the task in its data structure and then await it before starting the next one.

    Combining both of these:

    static Dictionary<int, Tuple<int, CancellationTokenSource, Task>> tokenSources = new Dictionary<int, Tuple<int, CancellationTokenSource, Task>>();
    
    private async Task IPCameraMethod()
    {
        if (string.IsNullOrWhiteSpace(textBoxUrl.Text) ||
            !int.TryParse(SourceComboBox.Name.Replace("comboBox", ""), out int cameraIndex) ||
            comboBoxSavedCameras.SelectedIndex < 0)
        {
            return;
        }
    
        string url = textBoxUrl.Text;
        int selectedItemIndex = comboBoxSavedCameras.SelectedIndex;
    
        if (tokenSources.TryGetValue(cameraIndex, out var tuple))
        {
            if (selectedItemIndex != tuple.Item1)
            {
                tuple.Item2.Cancel();
                tuple.Item2.Dispose();
                await tuple.Item3;
                tokenSources.Remove(cameraIndex);
            }
        }
    
        var newCts = new CancellationTokenSource();
        tokenSources[cameraIndex] = Tuple.Create(selectedItemIndex, newCts, (Task)null);
    
        Debug.WriteLine("CREATING NEW TASK");
        tokenSources[cameraIndex].Item3 = Task.Run(() =>
            Camera.IPCameraService.Main(SourceImageControl, selectedItemIndex, newCts.Token, url)
        );
    }