OnceAsync: run f# async function exactly once

Viewed 177

I'm trying to write a function (OnceAsync f) that ensures that an async function is run only once on a server (i.e. a multi-threaded environment). I thought it would be easy, but it became complicated quickly (locks, busy waits!!)

This is my solution, but I think it's over-engineered; there must be a better way. This should work in 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
    }

You can use this code to test if OnceAsync is correct:

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 Answers
Related