【问题标题】:How to "sleep" until timeout or cancellation is requested in .NET 4.0如何在 .NET 4.0 中请求超时或取消之前“休眠”
【发布时间】:2013-09-13 22:30:13
【问题描述】:

什么是睡眠一定时间的最佳方式,但能够被CancellationToken 中的IsCancellationRequested 打断?

我正在寻找适用于 .NET 4.0 的解决方案。

我想写

void MyFunc (CancellationToken ct)
{
   //... 
   // simulate some long lasting operation that should be cancelable 
   Thread.Sleep(TimeSpan.FromMilliseconds(10000), ct); 
}

【问题讨论】:

    标签: c# .net-4.0 sleep cancellation cancellationtokensource


    【解决方案1】:

    我刚刚在这里写了一篇博客:

    CancellationToken and Thread.Sleep

    简而言之:

    var cancelled = token.WaitHandle.WaitOne(TimeSpan.FromSeconds(5));
    

    在您的上下文中:

    void MyFunc (CancellationToken ct)
    {
       //... 
       // simulate some long lasting operation that should be cancelable 
       var cancelled = ct.WaitHandle.WaitOne(TimeSpan.FromSeconds(10));
    }
    

    【讨论】:

    • Onur 那么你就可以像那样做token.WaitHandle.WaitOne(5000)
    • Hower,文档建议避免使用 WaitHandle,除非必要:“访问此属性会导致 WaitHandle 被实例化。最好仅在必要时使用此属性,然后处置关联的尽早取消 CancellationTokenSource 实例(处置源将处置此分配的句柄)。” msdn.microsoft.com/EN-US/library/dd321769(v=VS.100,d=hv.2).aspx
    • 警告/注意:如果底层 CancellationTokenSource 被释放,访问 ct.WaitHandle 会引发异常。使用扩展方法很容易解决。ct.WaitForCancellation(timespan) 如果访问引发异常,它可以立即返回。应与取消令牌检查结合使用,因为这可能会导致误导性的“无等待”时间。
    • @sodjsn26fr,看看 CancellationTokenSource 类。它公开了一个 Token 属性。那是你的令牌。在父作用域中创建一个新的 CancellationTokenSource()。将它的 Token 属性传递到任何你想使用它的地方,比如在某个地方的循环中。如果调用了 tokenSource.Cancel() 方法,那么您的 Token 将被通知取消
    • @sodjsn26fr,另一件事。如果您将令牌添加为 Controller-methods (WebAPI) 中的最后一个参数,您将“免费”获得令牌。此令牌可用于取消服务器上的服务器请求
    【解决方案2】:

    另外,我认为这很清楚:

    Task.Delay(waitTimeInMs, cancellationToken).Wait(cancellationToken);

    【讨论】:

    • 不是 C#4.0 而是 C#4.5。
    • @Frode 的答案更好。原因?省略“Wait();”太容易了最后,没有编译错误,这根本不会引入任何延迟!受够了这个,我只是更喜欢完全避免这种语法。
    • 我认为您需要处理msdn.microsoft.com/en-us/library/…中记录的异常
    • 示例代码:尝试 { System.Threading.Tasks.Task.Delay(waitTimeInMs,cancellationToken).Wait(); } catch (OperationCanceledException) { } catch (AggregateException aex) { if (null != aex.InnerExceptions) { if (aex.InnerExceptions.Any(inn => inn.GetType() != typeof(System.Threading.Tasks.TaskCanceledException ))) { 扔; } } }
    • 真的有必要将 CancellationToken 传递给延迟和等待方法调用吗?如果给延迟方法的令牌已取消,Wait 不会继续阻塞。
    【解决方案3】:

    要在一定时间后取消异步操作,同时仍然能够手动取消操作,请使用以下内容

    CancellationTokenSource cts = new CancellationTokenSource();
    CancellationToken token = cts.Token;
    cts.CancelAfter(5000);
    

    这将导致五秒后取消。要取消您自己的操作,您只需将token 传递给您的异步方法并使用token.ThrowifCancellationRequested() 方法,您已经在某处设置了一个事件处理程序来触发cts.Cancel()

    所以一个完整的例子是:

    CancellationTokenSource cts = new CancellationTokenSource();
    CancellationToken token = cts.Token;
    cts.CancelAfter(5000);
    
    // Set up the event handler on some button.
    if (cancelSource != null)
    {
        cancelHandler = delegate
        {
            Cancel(cts);
        };
        stopButton.Click -= cancelHandler;
        stopButton.Click += cancelHandler;
    }
    
    // Now launch the method.
    SomeMethodAsync(token);
    

    其中stopButton是你点击取消正在运行的任务的按钮

    private void Cancel(CancellationTokenSource cts)
    {
        cts.Cancel();
    }
    

    方法定义为

    SomeMethodAsync(CancellationToken token)
    {
        Task t = Task.Factory.StartNew(() => 
            {
                msTimeout = 5000;
                Pump(token);
            }, token,
               TaskCreationOptions.None,
               TaskScheduler.Default);
    }
    

    现在,为了使您能够工作线程并启用用户取消,您需要编写一个“泵送”方法

    int msTimeout;
    bool timeLimitReached = false;
    private void Pump(CancellationToken token)
    {
        DateTime now = DateTime.Now;
        System.Timer t = new System.Timer(100);
        t.Elapsed -= t_Elapsed;
        t.Elapsed += t_Elapsed;
        t.Start();
        while(!timeLimitReached)
        {
            Thread.Sleep(250);
            token.ThrowIfCancellationRequested();
        }
    }
    
    void t_Elapsed(object sender, ElapsedEventArgs e)
    {
        TimeSpan elapsed = DateTime.Now - this.readyUpInitialised;
        if (elapsed > msTimeout)
        {
            timeLimitReached = true;
            t.Stop();
            t.Dispose();
        }
    }
    

    注意,SomeAsyncMethod 将直接返回给调用者。要同时阻止呼叫者,您必须将 Task 在呼叫层次结构中向上移动。

    【讨论】:

    • 感谢您非常详细的回答。但我认为您提出的解决方案不是我想要的。我想确定函数 in 的睡眠量,而不是调用函数的地方!关键是要模拟一些持久的操作。调用站点不关心如何记录日志。
    • 查看编辑。另外,请参阅 here 了解 SpinWaitSleep 之间的区别。
    • 我发现您的解决方案存在两个问题:1.) 它不会阻止执行,而是立即返回。这是一个“t.Wait();”可以解决。 2.) 只能在等待一定时间后取消,这毫无意义,因为反正我已经完成了。
    • 您的解决方案可能有效,但它看起来比我目前找到的最佳解决方案复杂得多:stackoverflow.com/a/18715183/1254743
    【解决方案4】:

    CancellationToken.WaitHandle 在 CancellationTokenSource 被释放后访问时会抛出异常:

    ObjectDisposedException: CancellationTokenSource 已被释放。

    在某些情况下,尤其是在手动处理 linked cancellation sources 时(应该如此),这可能会很麻烦。

    此扩展方法允许“安全取消等待”;但是,它应该与检查和正确标记取消令牌的状态和/或返回值的使用结合使用。这是因为它抑制了对 WaitHandle 的访问的异常,并且返回的速度可能比预期的要快。

    internal static class CancellationTokenExtensions
    {
        /// <summary>
        /// Wait up to a given duration for a token to be cancelled.
        /// Returns true if the token was cancelled within the duration
        /// or the underlying cancellation token source has been disposed.
        /// </summary>
        public static bool WaitForCancellation(this CancellationToken token, TimeSpan duration)
        {
            WaitHandle handle;
            try
            {
                handle = token.WaitHandle;
            }
            catch
            {
                /// The source of the token was disposed (already cancelled)
                return true;
            }
    
            if (handle.WaitOne(duration))
            {
                /// A cancellation occured during the wait
                return true;
            }
            else
            {
                /// No cancellation occured during the wait
                return false;
            }
        }
    }
    

    【讨论】:

      【解决方案5】:

      目前我发现的最佳解决方案是:

      void MyFunc(CancellationToken ct)
      {
        //...
        var timedOut = WaitHandle.WaitAny(new[] { ct.WaitHandle }, TimeSpan.FromMilliseconds(2000)) == WaitHandle.WaitTimeout;
        var cancelled = ! timedOut;
      }
      

      更新:

      目前最好的解决方案是accepted answer

      【讨论】:

        猜你喜欢
        • 2020-06-23
        • 1970-01-01
        • 2020-06-23
        • 2017-10-30
        • 2010-12-23
        • 1970-01-01
        • 1970-01-01
        • 2013-05-31
        • 1970-01-01
        相关资源
        最近更新 更多