我们需要一个定时器,它会在给定的时间间隔内触发一段时间,然后停止,我们想要异步等待定时器停止。当然,这可以使用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()