自定义计时器分辨率与System.Timers.Timer分辨率

时间:2018-05-10 13:54:50

标签: .net timer f# immutability

我们需要一个定时器,它会在给定的时间间隔内触发一段时间,然后停止,我们想要异步等待定时器停止。当然,这可以使用System.Timers.Timer类来完成,但它有点麻烦并涉及使用AutoResetEvent或类似的。为了简化这个用例,我创建了一个自定义计时器类。作为一个F#项目,我使我的计时器不可变并基于代理,但我也希望与.NET计时器的实现方式保持一致。我查看了System.Timers.Timer参考源,它似乎包含System.Threading.Timer,它使用Win32 QueryUnbiasedInterruptTime API。因此,我在创建计时器时使用相同的API来管理计时器间隔。但是,我在测试时观察了我的计时器和System.Timers.Timer计时器之间的不同行为。

当使用我的自定义计时器,间隔为1毫秒,超时为100毫秒时,计时器将触发约65次,每次事件之间约1.5毫秒。当使用间隔为1ms的System.Timers.Timer并在经过100ms后停止计时器(使用System.Diagnostics.Stopwatch检查已用时间)时,计时器仅在每个事件之间约15ms触发7次。这似乎更像System.Timers.Timer使用DateTime.Ticks然后使用QueryUnbiasedInterruptTime。我的自定义计时器比内置System.Timers.Timer的分辨率更高,我可能会遇到什么问题?我期待一致的行为,并惊讶地发现它们如此不同。

我还担心我必须添加到我的函数中以计算下一个间隔的特定行。我看到Elapsed事件在每个经过的时间间隔内发生两次而不是一次,原因是过去的时间比间隔少了一毫秒,所以它基本上将下一个间隔计算为零毫秒并立即解雇。我通过添加一个小于一毫秒的下一个间隔的检查并替换整个间隔来纠正这个,但这感觉就像一个黑客。我在代码中注释了这一行。

我的自定义计时器的代码如下:

open System

module private Timer =
    open System.Runtime.InteropServices
    open System.Runtime.Versioning

    [<DllImport("kernel32.dll")>]
    [<ResourceExposure(ResourceScope.None)>]
    extern bool QueryUnbiasedInterruptTime (int64& value)

    let inline private queryUnbiasedInterruptTime () =
        let mutable ticks = 0L
        if QueryUnbiasedInterruptTime &ticks
        then Some ticks
        else None

    /// Get the current timestamp in 100-ns increments 
    let getTicks () =
        match queryUnbiasedInterruptTime() with
        | Some ticks -> ticks
        | _ -> DateTime.UtcNow.Ticks

type private TimerMessage =
| Start
| Stop
| Wait of AsyncReplyChannel<unit>

type ImmutableTimer (interval: TimeSpan, ?timeout: TimeSpan) =
    let zero = TimeSpan.FromMilliseconds(0.0)
    let one = TimeSpan.FromMilliseconds(1.0)
    let elapsed = Event<unit>()
    let intervalTicks = interval.Ticks
    let getNextInterval startTime = 
        match intervalTicks - (Timer.getTicks() - startTime) |> TimeSpan.FromTicks with
        // This feels like a hack, but is required to fix a near-zero next interval
        | x when x < one -> interval
        | x when x > zero -> x
        | _ -> zero
    let agent =
        MailboxProcessor<TimerMessage>.Start
        <| fun inbox ->
            let rec loop isStarted (waiter: AsyncReplyChannel<unit> option) endTime (nextInterval: TimeSpan) =
                async {
                    let startTime = Timer.getTicks()
                    try                        
                        let! message = inbox.Receive(nextInterval.TotalMilliseconds |> Math.Ceiling |> int)
                        match message with
                        | Start -> 
                            match timeout with
                            | Some time -> return! getNextInterval startTime |> loop true waiter (Timer.getTicks() + time.Ticks |> DateTime.FromFileTimeUtc |> Some)
                            | None -> return! getNextInterval startTime |> loop true waiter None
                        | Stop -> 
                            match waiter with
                            | Some channel -> channel.Reply()
                            | None -> ()
                        | Wait channel -> 
                            return! getNextInterval startTime |> loop isStarted (Some channel) endTime
                    with | _ ->
                        if isStarted
                        then Async.Start <| async { elapsed.Trigger() }
                             match endTime with
                             | Some time ->
                                if DateTime.FromFileTimeUtc(Timer.getTicks()) < time
                                then return! getNextInterval startTime |> loop isStarted waiter endTime
                                else match waiter with
                                     | Some channel -> channel.Reply()
                                     | None -> ()
                             | None -> return! getNextInterval startTime |> loop isStarted waiter endTime
                        else return! getNextInterval startTime |> loop isStarted waiter endTime
                }
            interval |> loop false None None

    let start () = agent.Post Start
    let stop () = agent.Post Stop
    let wait () = agent.PostAndReply Wait
    let asyncWait () = agent.PostAndAsyncReply Wait

    new (interval, ?timeout) = 
        match timeout with
        | Some t -> ImmutableTimer(TimeSpan.FromMilliseconds interval, TimeSpan.FromMilliseconds t)
        | None -> ImmutableTimer(TimeSpan.FromMilliseconds interval)

    new (intervalTicks, ?timeoutTicks) = 
        match timeoutTicks with
        | Some t -> ImmutableTimer(TimeSpan.FromTicks intervalTicks, TimeSpan.FromTicks t)
        | None -> ImmutableTimer(TimeSpan.FromTicks intervalTicks)

    member __.Elapsed = elapsed.Publish
    member __.Start () = start()
    member __.Stop () = stop()
    member __.Wait () = wait()
    member __.AsyncWait () = asyncWait ()
    member __.StartAndWait () = start(); wait()
    member __.StartAndAsyncWait () = start(); asyncWait()

计时器的使用示例:

let mutable count = 0
let timer = ImmutableTimer(1.0, 100.0)
let stopwatch = new System.Diagnostics.Stopwatch()
timer.Elapsed |> Observable.subscribe (fun _ -> printfn "%A: %d" stopwatch.Elapsed <| System.Threading.Interlocked.Increment(&count))
stopwatch.Start()
timer.StartAndWait()
stopwatch.Stop()

0 个答案:

没有答案