如果过于频繁地调用,以下函数中的函数callWebServiceAsync
将被拒绝。完成可能需要几毫秒到一分钟。
F#
let doWorkAsync p = async { // doWorkAsync will be called many times
....
let! ret = callWebServiceAsync p' // need to be throttled, high volumn of requestion in a short time will cause blocking
....
let ret' = process ret
ret'
}
C#
async Task<T> DoWorkAsync(S p) // DoWorkAsync will be called many times
{
....
ret = await CallWebServiceAsync(...); // need to be throttled, high volumn of requestion in a short time will cause blocking
....
return Process(ret);
}
如何限制通话频率?我不确定他们如何检测呼叫,因此最好均匀地调用该功能(无突发请求)。
答案 0 :(得分:3)
My first reaction would be to use a MailboxProcessor
. That's how you generally force all calls to go through a single gateway.
Below is a throttle
function that will return an async continuation at most once per timespan. High level, it
inbox.Receive
). This request contains a channel to return the results.chan.Reply
)The code is as follows
let createThrottler (delay: TimeSpan) =
MailboxProcessor.Start(fun inbox ->
let rec loop (lastCallTime: DateTime option) =
async {
let! (chan: AsyncReplyChannel<_>) = inbox.Receive()
let sleepTime =
match lastCallTime with
| None -> 0
| Some time -> int((time - DateTime.Now + delay).TotalMilliseconds)
if sleepTime > 0 then
do! Async.Sleep sleepTime
let lastCallTime = DateTime.Now
chan.Reply()
return! loop(Some lastCallTime)
}
loop None)
Then you can use it like this:
[<EntryPoint>]
let main argv =
// Dummy implementation of callWebServiceAsync
let sw = Stopwatch.StartNew()
let callWebServiceAsync i =
async {
printfn "Start %d %d" i sw.ElapsedMilliseconds
do! Async.Sleep(100)
printfn "End %d %d" i sw.ElapsedMilliseconds
return i
}
// Create a throttler MailboxProcessor and then the throttled function from that.
let webServiceThrottler = createThrottler (TimeSpan.FromSeconds 1.)
let callWebServiceAsyncThrottled i =
async {
do! webServiceThrottler.PostAndAsyncReply(id)
return! callWebServiceAsync i
}
// Some tests
Async.Start(async { let! i = callWebServiceAsyncThrottled 0
printfn "0 returned %d" i
let! i = callWebServiceAsyncThrottled 1
printfn "1 returned %d" i
let! i = callWebServiceAsyncThrottled 2
printfn "2 returned %d" i })
Async.Start(callWebServiceAsyncThrottled 3 |> Async.Ignore)
Async.Start(callWebServiceAsyncThrottled 4 |> Async.Ignore)
Async.Start(callWebServiceAsyncThrottled 5 |> Async.Ignore)
Async.Start(callWebServiceAsyncThrottled 6 |> Async.Ignore)
Console.ReadLine() |> ignore
0
If you run this, you'll see that it throttles your calls to that service as desired, no matter whether you're running in parallel or in series or both.
答案 1 :(得分:2)
如果要将呼叫频率限制在毫秒范围内,则必须使用Win32调用来获取时间戳,其分辨率高于使用System.DateTime
时通常可用的分辨率。我可能会使用QueryUnbiasedInterruptTime
来以100ns的增量获得时间。然后,您可以跟踪上次拨打电话并异步休眠直到时间间隔过去,使用锁定将更新同步到上次呼叫时间:
open System
open System.Runtime.InteropServices
open System.Runtime.Versioning
open System.Threading
// Wrap the Win32 call to get the current time with 1-millisecond resolution
module private Timestamp =
[<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 milliseconds
let get () =
match queryUnbiasedInterruptTime() with
| Some ticks -> ticks / 1000L
| _ -> DateTime.UtcNow.Ticks / TimeSpan.TicksPerMillisecond
// Stub for request and response types
type Request = Request
type Response = Response
// Minimum time between calls (ms) =
let callFrequencyThreshold = 10L
// A wrapper around the call to the service that will throttle the requests
let doWorkAsync : Request -> Async<Response> =
// Milliseconds since last call to service
let mutable lastCallTime = 0L
// Lock to protect the last call time
let syncRoot = obj()
// The real call to the service
let callService request =
async {
// Simulate work
do! Async.Sleep 1000
return Response
}
// Accept each request and wait until the threshold has elapsed to call the service
fun request ->
async {
let rec run () =
lock syncRoot <| fun () ->
async {
let currentTime = Timestamp.get()
if currentTime - lastCallTime > callFrequencyThreshold
then lastCallTime <- currentTime
return! callService request
else do! Async.Sleep <| int (callFrequencyThreshold - (currentTime - lastCallTime))
return! run ()
}
return! run ()
}
但是,除非绝对必要,否则我不会建议采用基于时间的节流方法。就个人而言,我倾向于像信号量这样的东西来限制对服务的并发调用次数。这样,如果需要,您可以确保一次只调用一次服务,或者根据环境等允许一次调用服务n
。它还显着简化了代码,在我看来,提供了更可靠的实施:
open System.Threading
// Stub for request and response types
type Request = Request
type Response = Response
// A wrapper around the call to the service that will throttle the requests
let doWorkAsync : Request -> Async<Response> =
// A semaphore to limit the number of concurrent calls
let concurrencyLimit = 10
let semaphore = new SemaphoreSlim(concurrencyLimit, concurrencyLimit)
// The real call to the service
let callService request =
async {
// Simulate work
do! Async.Sleep 1000
return Response
}
// Accept each request, wait for a semaphore token to be available,
// then call the service
fun request ->
async {
do! semaphore.WaitAsync() |> Async.AwaitTask
return! callService request
}