Task.Delay没有被取消?

9

我正在尝试为一款游戏构建界面。这个游戏持续1分钟。 GetStop 方法在60秒后停止游戏。 play 方法开始游戏,而 quit 方法则退出游戏。现在理想情况下,如果我在30秒后退出游戏,则计时器应该被重置,当我再次点击“播放”按钮时,计时器应该再次运行1分钟。这样下一局游戏将运行1分钟。如果我再次按下“退出”按钮,则计时器应为下一场比赛重置。

然而,我的代码似乎存在某些问题。每当我执行 quit 方法时,计时器似乎会保留在那个状态。所以,如果我在30秒内退出比赛,那么下一场比赛只会持续30秒。如果我在50秒时退出比赛,那么下一场比赛将只持续10秒。理想情况下,计时器应该被重置,但它未被重置。

我已经没有更多的想法了。有人能提供一些建议吗?

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;
        }
    //
}

4
我不确定我是否正确理解了你的问题,但也许你认为取消一个“任务”会停止进程的运行。这是不正确的。只有作为进程包装器的任务本身将停止等待它,并且您的代码余下部分将继续执行,包括“任务”中的进程。 - Silvermind
2
你的代码只能在你允许的地方停止。例如,如果你设置了一个5分钟的休眠时间,那么这个休眠时间将会继续下去。你需要的是在游戏循环中将时间作为更新/处理条件的一部分。不要调用"getstop",而是在开始时设置一个变量为now+<timespan>,然后检查now < timespan。 - BugFinder
谢谢,您能否给我发送一个示例或参考链接以供参考? - David Silwal
如果您使用任务处理游戏循环,则可能不会对此问题的解决方案感到满意。请查看此处有关游戏循环通常如何工作的描述: https://dev59.com/FXTYa4cB1Zd3GeqPpwac#17440807 - nvoigt
2个回答

2
我可以看到你的代码可能会在几个地方引发异常。如果你捕获并忽略了所有异常,你可能无法看到时间、取消令牌和任务不正确的原因。
首先,我可以确定以下问题:
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;
        }
}

我创建了一个控制台应用程序,进行了一些小的修改,在这里它似乎运行得很好。请看一下:

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;
    }
}

我建议您也可以看一下System.Threading.Timer,或许对某些场景有帮助...祝您的游戏好运!

@DavidSilwal,很高兴能帮到你。你可以接受其中一个答案吗?谢谢。 - Anderson Rancan

1

为了满足我的需要,我创建了一个叫做CancellableTask的包装器,它可能会帮助你实现你想要的功能。你可以通过将delegate作为构造函数的参数来创建任务,然后可以在延迟或不延迟的情况下运行它。它可以在任何时候被Canceled,无论是在延迟期间还是在运行期间。

这是这个类:

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));
        }

    }

"而它可以像这样使用:"
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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接