Нам нужен таймер, который срабатывает с заданным интервалом в течение определенного промежутка времени, затем останавливается, и мы хотим асинхронно ждать, пока таймер не остановится.Конечно, это можно сделать с помощью класса 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()