Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Task.Delay didn’t get canceled?

I am trying to build an interface for a game. The game runs for 1 minute. The GetStop method stops after 60 sec game. The play method starts the game and the quit method quit the game. Now ideally what I want is when I quit the game after 30 seconds, the timer should get reset and on click of the Play button, the timer should again run for 1 minute. So that the next game gets to run for 1 minute. If I press Quit button then again, the timer should be reset for the next game.

However, there seems to be a certain issue in my code. Whenever I execute quit method the timer seems to be saved at that state. So, If I quit a race in 30 seconds then the next race will last for only 30 seconds. If I quit a race in 50 seconds, the next race will last only 10 seconds. Ideally, the timer should get reset but it is not getting reset.

I am out of ideas here. Can anyone please provide some suggestions??

private async Task GetStop(CancellationToken token)
{ 
    await Task.Run(async () =>
    {
        token.ThrowIfCancellationRequested();
        await Task.Delay(TimeSpan.FromSeconds(60), token);

        token.ThrowIfCancellationRequested();
        if (!token.IsCancellationRequested)
        {
            sendMessage((byte)ACMessage.AC_ESCAPE); 
        }
    }, token);
}

public async void Play()
{         
        sendMessage((byte)ACMessage.AC_START_RACE); 
        _cts.Cancel();

        if (_cts != null)
        {
            _cts.Dispose();
            _cts = null;
        }
        _cts = new CancellationTokenSource(); 
        await GetStop(_cts.Token);
   }

public void Quit()
{
        _cts.Cancel();
        if (_cts != null)
        {
            _cts.Dispose();
            _cts = null;
        }
    //
}
like image 788
David Silwal Avatar asked Jul 02 '18 07:07

David Silwal


2 Answers

I can see that your code may throw exceptions at several places. If you are catching and ignoring all exceptions, you may not be able to see the reason why the time, cancellation token and tasks are not working correctly.

At a first moment, I could identify the following:

private async Task GetStop(CancellationToken token)
{ 
    await Task.Run(async () =>
    {
        // I think you don't need to throw here
        token.ThrowIfCancellationRequested();

        // this will throw an Exception when cancelled
        await Task.Delay(TimeSpan.FromSeconds(60), token); 

        // again, I think you don't need to throw here
        token.ThrowIfCancellationRequested();

        if (!token.IsCancellationRequested)
        {
            sendMessage((byte)ACMessage.AC_ESCAPE); 
        }
    }, token);
}

public async void Play()
{         
        sendMessage((byte)ACMessage.AC_START_RACE); 

        // at some scenarios this may be null
        _cts.Cancel();

        if (_cts != null)
        {
            _cts.Dispose();
            _cts = null;
        }
        _cts = new CancellationTokenSource(); 
        await GetStop(_cts.Token);
   }

public void Quit()
{
        _cts.Cancel();
        if (_cts != null)
        {
            _cts.Dispose();
            _cts = null;
        }
}

I created a Console application, did some small modifications, and here it seems to work just fine. Please take a look:

public static class Program
{
    public static void Main(string[] args)
    {
        var game = new Game();

        game.Play();
        Task.Delay(5000).Wait();
        game.Quit();

        game.Play();
        Task.Delay(15000).Wait();
        game.Quit();

        game.Play();
        Task.Delay(65000).Wait();

        Console.WriteLine("Main thread finished");
        Console.ReadKey();

        // Output:
        //
        // Start race (-00:00:00.0050018)
        // Quit called (00:00:05.0163131)
        // Timeout (00:00:05.0564685)
        // Start race (00:00:05.0569656)
        // Quit called (00:00:20.0585092)
        // Timeout (00:00:20.1025051)
        // Start race (00:00:20.1030095)
        // Escape (00:01:20.1052507)
        // Main thread finished
    }
}

internal class Game
{
    private CancellationTokenSource _cts;

    // this is just to keep track of the behavior, should be removed
    private DateTime? _first;
    private DateTime First
    {
        get
        {
            if (!_first.HasValue) _first = DateTime.Now;
            return _first.Value;
        }
    }


    private async Task GetStop(CancellationToken token)
    {
        await Task.Run(async () =>
        {
            try
            {
                // we expect an exception here, if it is cancelled
                await Task.Delay(TimeSpan.FromSeconds(60), token);
            }
            catch (Exception)
            {
                Console.WriteLine("Timeout ({0})", DateTime.Now.Subtract(First));
            }

            if (!token.IsCancellationRequested)
            {
                Console.WriteLine("Escape ({0})", DateTime.Now.Subtract(First));
            }
        }, token);
    }

    public async void Play()
    {
        Console.WriteLine("Start race ({0})", DateTime.Now.Subtract(First));

        CancelAndDisposeCts();

        _cts = new CancellationTokenSource();
        await GetStop(_cts.Token);
    }

    public void Quit()
    {
        Console.WriteLine("Quit called ({0})", DateTime.Now.Subtract(First));
        CancelAndDisposeCts();
    }

    private void CancelAndDisposeCts()
    {
        // avoid copy/paste for the same behavior
        if (_cts == null) return;

        _cts.Cancel();
        _cts.Dispose();
        _cts = null;
    }
}

I would also suggest to take a look on System.Threading.Timer, maybe if can be useful for some scenarios...

Good luck with your game!

like image 109
Anderson Rancan Avatar answered Sep 20 '22 20:09

Anderson Rancan


For my own purposes I created a wrapper called CancellableTask which might help you achieve what you want. You can create the task using by passing a delegate as a parameter to the constructor, then you can Run it with delay or without. It can be Canceled at any time, either during the delay or while it's running.

Here's the class:

public class CancellableTask
    {
        private CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
        private Task cancellationTask = null;
        private Action<Task> method;
        private int delayMilis;

        public bool Delayed { get; private set; }

        public TaskStatus TaskStatus => cancellationTask.Status;

        public CancellableTask(Action<Task> task)
        {
            method = task;
        }

        public bool Cancel()
        {
            if (cancellationTask != null && (cancellationTask.Status == TaskStatus.Running || cancellationTask.Status == TaskStatus.WaitingForActivation))
            {
                cancellationTokenSource.Cancel();
                cancellationTokenSource.Dispose();
                cancellationTokenSource = new CancellationTokenSource();
                return true;
            }
            return false;
        }

        public void Run()
        {
            Delayed = false;
            StartTask();
        }

        public void Run(int delayMiliseconds)
        {
            if(delayMiliseconds < 0)
                throw new ArgumentOutOfRangeException();

            Delayed = true;
            delayMilis = delayMiliseconds;
            StartDelayedTask();
        }

        private void DelayedTask(int delay)
        {
            CancellationToken cancellationToken = cancellationTokenSource.Token;
            try
            {
                cancellationTask =
                    Task.
                        Delay(TimeSpan.FromMilliseconds(delay), cancellationToken).
                        ContinueWith(method, cancellationToken);

                while (true)
                {
                    if (cancellationTask.IsCompleted)
                        break;

                    if (cancellationToken.IsCancellationRequested)
                    {
                        cancellationToken.ThrowIfCancellationRequested();
                        break;
                    }
                }
            }
            catch (Exception e)
            {
                //handle exception
                return;
            }

        }

        private void NormalTask()
        {
            CancellationToken cancellationToken = cancellationTokenSource.Token;
            try
            {
                cancellationTask =
                    Task.Run(() => method, cancellationToken);

                while (true)
                {
                    if (cancellationTask.IsCompleted)
                        break;

                    if (cancellationToken.IsCancellationRequested)
                    {
                        cancellationToken.ThrowIfCancellationRequested();
                        break;
                    }
                }
            }
            catch (Exception e)
            {
                //handle exception
                return;
            }
        }

        private void StartTask()
        {
            Task.Run(() => NormalTask());
        }

        private void StartDelayedTask()
        {
            Task.Run(() => DelayedTask(delayMilis));
        }

    }

And it can be used like this:

var task = new CancellableTask(delegate
            {
               DoSomething(); // your function to execute
            });
task.Run(); // without delay
task.Run(5000); // with delay in miliseconds
task.Cancel(); // cancelling the task
like image 31
AndrejH Avatar answered Sep 22 '22 20:09

AndrejH