简体   繁体   中英

truly lazy cache pattern? F#

I have the following type for implementing a simple lazy cache:

module CachedFoo =

let mutable private lastAccess:Option<DateTime> = None

// returns a lazy value that initializes the cache when
// accessed for the first time (safely)
let private createCacheInitialization() =
    lazy(
        let someObject = SomeLongRunningOperation()
        lastAccess <- Option.Some(DateTime.Now)
        someObject
    )

// current cache represented as lazy value
let mutable private currentCache = createCacheInitialization()

// Reset - cache will be re-initialized next time it is accessed
// (this doesn't actually initialize a cache - just creates a lazy value)
let MaybeReset() =
    if (lastAccess.IsSome && DateTime.Now > (lastAccess.Value + TimeSpan.FromSeconds (10.0))) then
        currentCache <- createCacheInitialization()

let GetCache() =
    MaybeReset()
    currentCache.Value

First question: is the above thread-safe? It seems lazy() is thread-safe by default, but I guess I need to put some locking around the assignment of the lastAccess field?

Second and most important: this is lazy in the sense that its value is not retrieved until someone demands for it, however, I think I could even do it more lazy by returning the last cached object even in the case that Reset() is called, but launching an async thread in the background that would call this method.

In C# it would be something like this:

public SomeObject GetCache() {
    try {
        return currentCache.Value;
    } finally {
        ThreadPool.QueueUserWorkItem(new WaitCallback(MaybeReset));
    }
}

How would I do that in F#? (Bonus points if solution uses fancy async stuff instead of using ThreadPool API).

I think updating lastAccess is thread-safe for two reasons

  • you only do it inside the lazy which means that it's only going to be updated once anyway (though there may be a more subtle race with Reset , I'm not certain)

  • lastAccess is a single reference (to Option ) and so will be updated atomically anyway

To kick off a new "fire and forget" async to re-calculate the value, do something like this:

let GetCache() =
    let v = currentCache.Value // to make sure we get the old one
    async { MaybeReset() } |> Async.Start
    v

Thanks to Ganesh's insight, I finally went for this solution that doesn't make the 2nd requestor wait on the result while it's being refreshed:

module CachedFoo =

let mutable private lastAccess:Option<DateTime> = None

// returns a lazy value that initializes the cache when
// accessed for the first time (safely)
let private createCacheInitialization() =
    lazy(
        let someObject = SomeLongRunningOperation()
        lastAccess <- Option.Some(DateTime.Now)
        someObject
    )

// current cache represented as lazy value
let mutable private currentCache = createCacheInitialization()

let lockObject = new Object()

let timeout = TimeSpan.FromSeconds (10.0)

// Reset - cache will be re-initialized next time it is accessed
// (this doesn't actually initialize a cache - just creates a lazy value)
let MaybeReset() =
    lock lockObject (fun () ->
        if (lastAccess.IsSome && DateTime.Now > (lastAccess.Value + timeout)) then
            let newCache = createCacheInitialization()
            ignore(newCache.Force())
            currentCache <- newCache
    )

let GetCache() =
    let v = currentCache.Value // to make sure we get the old one
    async { MaybeReset() } |> Async.Start
    v

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM