with cache_lock.reader_lock:
x = cache_fetch_under_rlock(params)
if x is not None:
return x
# cache miss; must populate
with cache_lock.writer_lock:
x = cache_fetch_under_rlock(params)
if x is not None:
# oops, we didn't need to write, oh well
return x
x = do_the_expensive_thing(params)
cache_write_under_wlock(params, x)
return x
# do_the_expensive_thing may repeat this pattern with a different lock,
# if it has its own cache or pipelined single source of truth. This
# is a nestable, generic pattern for maximizing cache throughput
# while provably not duplicating the expensive thing. It has the
# disadvantage of holding the cache lock for the entire Expensive Thing
# but this is often appropriate; if it is worse to unnecessarily hold
# the writer lock than it is to duplicate work, however...
with cache_lock.reader_lock:
x = cache_fetch_under_rlock(params)
if x is not None:
return x
z = do_the_expensive_thing(params)
with cache_lock.writer_lock:
x = cache_fetch_under_rlock(params)
if x is not None:
# oops
return x # it is more GC-memory-efficient to throw z away;
# under garbage collection, prefer old objects
cache_write_under_wlock(params, z)
return z
Comments