Пользовательское разрешение таймера против разрешения System.Timers.Timer - PullRequest
0 голосов
/ 10 мая 2018

Нам нужен таймер, который срабатывает с заданным интервалом в течение определенного промежутка времени, затем останавливается, и мы хотим асинхронно ждать, пока таймер не остановится.Конечно, это можно сделать с помощью класса System.Timers.Timer, но это немного громоздко и предполагает использование AutoResetEvent или аналогичного.Чтобы упростить этот вариант использования, я создал собственный класс таймера.Будучи проектом F #, я сделал свой таймер неизменным и основал его на агенте, но я также хотел быть последовательным в том, как реализованы таймеры .NET.Я взглянул на справочный источник System.Timers.Timer, и он, похоже, обернул System.Threading.Timer, который использует Win32 QueryUnbiasedInterruptTime API.Поэтому при создании таймера я использовал тот же API для управления интервалом таймера.Тем не менее, при тестировании я наблюдаю различное поведение моего таймера и таймера System.Timers.Timer.

При использовании моего пользовательского таймера с интервалом 1 мс и тайм-аутом 100 мс таймер сработает примерно 65 раз с интервалом примерно 1,5 мс.между каждым событием.При использовании System.Timers.Timer с интервалом 1 мс и остановке таймера по истечении 100 мс (при использовании System.Diagnostics.Stopwatch для проверки истекшего времени) таймер срабатывает только 7 раз с интервалом около 15 мс между каждым событием.Это похоже на то, что 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()
...