.net 如何保持取消任务直到满足条件(TaskCanceledException)

xfb7svmp  于 2023-01-31  发布在  .NET
关注(0)|答案(5)|浏览(150)

我想在引发事件时经过一些延迟之后调用一个方法,但是任何后续事件都应该“重新启动”这个延迟。快速举例说明,视图应该在滚动条位置改变时更新,但仅在用户完成滚动后1秒。
现在我可以看到很多实现的方法,但最直观的是使用Task.Delay + ContinueWith +取消令牌。然而,我遇到了一些问题,更准确地说,后续调用我的函数导致TaskCanceledException异常,我开始想知道我如何才能摆脱它。以下是我的代码:

private CancellationTokenSource? _cts;

private async void Update()
{
    _cts?.Cancel();
    _cts = new();
    await Task.Delay(TimeSpan.FromSeconds(1), _cts.Token)
       .ContinueWith(o => Debug.WriteLine("Update now!"), 
       TaskContinuationOptions.OnlyOnRanToCompletion);
}

我已经找到了一个工作相当不错的变通办法,但我想使第一个想法的工作。

private CancellationTokenSource? _cts;
private CancellationTokenRegistration? _cancellationTokenRegistration;

private void Update()
{
    _cancellationTokenRegistration?.Unregister();
    _cts = new();
    _cancellationTokenRegistration = _cts.Token.Register(() => Debug.WriteLine("Update now!"));
    _cts.CancelAfter(1000);
}
aurhwmvo

aurhwmvo1#

根据我自己的经验,我处理过很多类似你描述的场景,例如,* 在鼠标停止移动一秒后更新某个东西 * 等等。
很长一段时间,我会用你描述的方法来重启计时器,即取消一个旧任务,然后启动一个新任务。但我从来不喜欢这种混乱的方式,所以我想出了一个替代方案,在生产代码中使用。长期来看,它被证明是相当可靠的。它利用了捕获到的与任务相关的上下文。TaskCanceledException的多个示例不再出现。

class WatchDogTimer
{
    int _wdtCount = 0;
    public TimeSpan Interval { get; set; } = TimeSpan.FromSeconds(1);
    public void Restart(Action onRanToCompletion)
    {
        _wdtCount++;
        var capturedCount = _wdtCount;
        Task
            .Delay(Interval)
            .GetAwaiter()
            .OnCompleted(() =>
            {
                // If the 'captured' localCount has not changed after awaiting the Interval, 
                // it indicates that no new 'bones' have been thrown during that interval.        
                if (capturedCount.Equals(_wdtCount))
                {
                    onRanToCompletion();
                }
            });
    }
}

另一个不错的好处是,它不依赖于平台计时器,在iOS/Android中和在WinForms/WPF中一样好用。
为了便于演示,可以在一个快速控制台演示中执行此操作,其中MockUpdateView()操作以500 ms的间隔发送到WDT 10次,但它只执行一次,即在收到最后一次重新启动后500 ms。

static void Main(string[] args)
    {
        Console.Title = "Test WDT";
        var wdt = new WatchDogTimer { Interval = TimeSpan.FromMilliseconds(500) };

        Console.WriteLine(DateTime.Now.ToLongTimeString());

        // "Update view 500 ms after the last restart."
        for (int i = 0; i < 10; i++)
        {
            wdt.Restart(onRanToCompletion: ()=>MockUpdateView());
            Thread.Sleep(TimeSpan.FromMilliseconds(500));
        }
        Console.ReadKey();
    }
    static void MockUpdateView()
    {
        Console.WriteLine($"Update now! WDT expired {DateTime.Now.ToLongTimeString()}");
    }
}

因此,通过500 ms乘以10次重启,这验证了从启动开始5秒时的一个事件。

ruyhziif

ruyhziif2#

您应该考虑使用微软的被动框架(又名Rx)-NuGet System.Reactive并添加using System.Reactive.Linq;
您没有说明您正在使用的UI,因此对于Windows窗体也添加System.Reactive.Windows.Forms,对于WPF添加System.Reactive.Windows.Threading
然后您可以执行以下操作:

Panel panel = new Panel(); // assuming this is a scrollable control

IObservable<EventPattern<ScrollEventArgs>> query =
    Observable
        .FromEventPattern<ScrollEventHandler, ScrollEventArgs>(
            h => panel.Scroll += h,
            h => panel.Scroll -= h)
        .Select(sea => Observable.Timer(TimeSpan.FromSeconds(1.0)).Select(_ => sea))
        .Switch();
    
IDisposable subscription = query.Subscribe(sea => Console.WriteLine("Hello"));

query为每个Scroll事件触发,并启动一个1秒计时器。Switch操作符监视每个Timer生成,并仅连接到最新生成的事件,从而忽略先前的Scroll事件。
就是这样。
滚动暂停1秒后,单词"Hello"将写入控制台。如果您再次开始滚动,则在每隔1秒暂停后,它将再次触发。

bnlyeluc

bnlyeluc3#

你可以合并一个状态变量和一个延迟来避免干扰定时器或者任务取消。这要简单得多IMO。
将此状态变量添加到类/窗体中:

private DateTime _nextRefresh = DateTime.MaxValue;

以下是您的刷新方式:

private async void Update() 
{
    await RefreshInOneSecond();
}

private async Task RefreshInOneSecond()
{
    _nextRefresh = DateTime.Now.AddSeconds(1);
    await Task.Delay(1000);
    if (_nextRefresh <= DateTime.Now)
    {
        _nextRefresh = DateTime.MaxValue;
        Refresh();
    }
}

如果您重复调用RefreshInOneSecond,它会将_nextRefresh时间戳推到以后,因此任何正在进行的刷新都不会起任何作用。
Demo on DotNetFiddle

798qvoo8

798qvoo84#

一种方法是创建一个计时器,并在用户执行操作时重置它。

timer = new Timer(1000);
timer.SynchronizingObject = myControl; // Needs a winforms object for synchronization
timer.Elapsed += OnElapsed; 
timer.Start(); // Don't forget to stop the timer whenever you are done

...
private void OnUserUpdate(){
    timer.Interval = 1000; // Setting the interval will reset the timer
}

有多个计时器可供选择,我相信其他计时器也可能有相同的模式。如果您使用WPF,DispatchTimer可能是最合适的。
请注意,System.Timers.TimerTask.Delay都在后台使用System.Threading.Timer。可以直接使用它,只需调用.Change方法来重置它。但请注意,这会在任务池线程上引发事件,因此您需要提供自己的同步。

6za6bjd0

6za6bjd05#

我在一个JavaScript应用程序中使用Timer实现了相同的场景。我相信在.NET世界中也是一样的。无论如何,当用户使用Task.Delay()重复调用一个方法时,处理这种用例会给GC和线程池带来更大的压力

var timer = new Timer()
{
    Enabled = true,
    Interval = TimeSpan.FromSeconds(5).TotalMilliseconds,
};

timer.Elapsed += (sender, eventArgs) =>
{
    timer.Stop();
    // do stuff
}
    
void OnKeyUp()
{
    timer.Stop();
    timer.Start();
}

相关问题