节流 - 最多每N毫秒调用一次函数?

时间:2018-06-02 19:46:49

标签: c# f#

如果过于频繁地调用,以下函数中的函数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);
}

如何限制通话频率?我不确定他们如何检测呼叫,因此最好均匀地调用该功能(无突发请求)。

2 个答案:

答案 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

  1. grabs an invocation requests from the message queue (inbox.Receive). This request contains a channel to return the results.
  2. checks if there's any need to delay from the previous run, and sleeps
  3. notes the start time of the current invocation (note you could swap this and step 4 if you want to throttle based on the end time of the invocations)
  4. triggers the caller (chan.Reply)
  5. loops to grab another request

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
        }