OnceAsync:运行f#async函数一次

时间:2017-06-01 00:26:19

标签: asynchronous f#

我正在尝试编写一个函数(OnceAsync f),它确保异步函数只在服务器上运行一次(即多线程环境)。我觉得这很容易,但很快变得复杂(锁,忙等待!!)

这是我的解决方案,但我认为它是过度设计的;肯定有更好的办法。这应该在FSI中起作用:

let locked_counter init =
    let c = ref init
    fun x -> lock c <| fun () -> 
        c := !c + x
        !c
let wait_until finished = async {  
    while not(finished()) do
        do! Async.Sleep(1000) 
}

let OnceAsync f = 
    // - ensure that the async function, f, is only called once
    // - this function always returns the value, f()
    let mutable res = None
    let lock_inc = locked_counter 0

    async {
        let count = lock_inc 1

        match res, count with
        | None, 1 ->    // 1st run
            let! r = f
            res <- Some r
        | None, _ ->    // nth run, wait for 1st run to finish
            do! wait_until (fun() -> res.IsSome)
        | _ -> ()       // 1st run done, return result

        return res.Value
    }

您可以使用此代码测试OnceAsync是否正确:

let test() =
    let mutable count = 0

    let initUser id = async {
        do! Async.Sleep 1000 // simulate work
        count <- count + 1
        return count
    }

    //let fmem1 = (initUser "1234")
    let fmem1 = OnceAsync (initUser "1234")

    async {
        let ps = Seq.init 20 (fun i -> fmem1)
        let! rs = ps |> Async.Parallel
        printfn "rs = %A" rs     // outputs: [|1; 1; 1; 1; 1; ....; 1|]
    }

test() |> Async.Start 

1 个答案:

答案 0 :(得分:3)

如果符合,最简单的方法是使用Async.StartChild。与您的解决方案不同,它会导致函数运行,即使结果从未实际使用过,例如在Seq.init 0案例中。

//let fmem1 = OnceAsync (initUser "1234")

async {
  let! fmem1 = Async.StartChild (initUser "1234")
  let ps = Seq.init 20 (fun i -> fmem1)
  let! rs = ps |> Async.Parallel
  printfn "rs = %A" rs     // outputs: [|1; 1; 1; 1; 1; ....; 1|]
} |> Async.RunSynchronously

与您最相似的最简单的方法是使用TaskCompletionSource,如下所示:

let OnceAsync f = 
  let count = ref 0
  let tcs = TaskCompletionSource<_>()
  async {
    if Interlocked.Increment(count) = 1 then
      let! r = f
      tcs.SetResult r
    return! Async.AwaitTask tcs.Task
  }

更实用的方法是使用MailboxProcessor并让它在第一次运行后缓存结果,并将其响应给所有后续请求。

let OnceAsync f = 
  let handler (agent: MailboxProcessor<AsyncReplyChannel<_>>) =
    let rec run resultOpt =
      async {
        let! chan = agent.Receive()
        let! result = 
          match resultOpt with
          | None -> f
          | Some result -> async.Return result
        chan.Reply result
        return! run (Some result)
      }
    run None
  let mbp = MailboxProcessor.Start handler
  async { return! mbp.PostAndAsyncReply id }