Add locks to make the caches well behaved in multi-threaded code.
Store builtins in cell variables to speed-up the common path, reducing the chance of a lock needing to block at all.
This commit is contained in:
parent
d9e8cc6249
commit
cbe8813f18
|
@ -15,6 +15,10 @@ from _functools import partial, reduce
|
||||||
from collections import OrderedDict, Counter
|
from collections import OrderedDict, Counter
|
||||||
from heapq import nsmallest
|
from heapq import nsmallest
|
||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
|
try:
|
||||||
|
from _thread import allocate_lock as Lock
|
||||||
|
except:
|
||||||
|
from _dummy_thread import allocate_lock as Lock
|
||||||
|
|
||||||
# update_wrapper() and wraps() are tools to help write
|
# update_wrapper() and wraps() are tools to help write
|
||||||
# wrapper functions that can handle naive introspection
|
# wrapper functions that can handle naive introspection
|
||||||
|
@ -115,37 +119,42 @@ def lfu_cache(maxsize=100):
|
||||||
http://en.wikipedia.org/wiki/Cache_algorithms#Least-Frequently_Used
|
http://en.wikipedia.org/wiki/Cache_algorithms#Least-Frequently_Used
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def decorating_function(user_function):
|
def decorating_function(user_function, tuple=tuple, sorted=sorted, len=len):
|
||||||
cache = {} # mapping of args to results
|
cache = {} # mapping of args to results
|
||||||
use_count = Counter() # times each key has been accessed
|
use_count = Counter() # times each key has been accessed
|
||||||
kwd_mark = object() # separate positional and keyword args
|
kwd_mark = object() # separate positional and keyword args
|
||||||
|
lock = Lock()
|
||||||
|
|
||||||
@wraps(user_function)
|
@wraps(user_function)
|
||||||
def wrapper(*args, **kwds):
|
def wrapper(*args, **kwds):
|
||||||
key = args
|
key = args
|
||||||
if kwds:
|
if kwds:
|
||||||
key += (kwd_mark,) + tuple(sorted(kwds.items()))
|
key += (kwd_mark,) + tuple(sorted(kwds.items()))
|
||||||
use_count[key] += 1 # count a use of this key
|
|
||||||
try:
|
try:
|
||||||
result = cache[key]
|
with lock:
|
||||||
wrapper.hits += 1
|
use_count[key] += 1 # count a use of this key
|
||||||
|
result = cache[key]
|
||||||
|
wrapper.hits += 1
|
||||||
except KeyError:
|
except KeyError:
|
||||||
result = user_function(*args, **kwds)
|
result = user_function(*args, **kwds)
|
||||||
cache[key] = result
|
with lock:
|
||||||
wrapper.misses += 1
|
use_count[key] += 1 # count a use of this key
|
||||||
if len(cache) > maxsize:
|
cache[key] = result
|
||||||
# purge the 10% least frequently used entries
|
wrapper.misses += 1
|
||||||
for key, _ in nsmallest(maxsize // 10,
|
if len(cache) > maxsize:
|
||||||
use_count.items(),
|
# purge the 10% least frequently used entries
|
||||||
key=itemgetter(1)):
|
for key, _ in nsmallest(maxsize // 10,
|
||||||
del cache[key], use_count[key]
|
use_count.items(),
|
||||||
|
key=itemgetter(1)):
|
||||||
|
del cache[key], use_count[key]
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def clear():
|
def clear():
|
||||||
"""Clear the cache and cache statistics"""
|
"""Clear the cache and cache statistics"""
|
||||||
cache.clear()
|
with lock:
|
||||||
use_count.clear()
|
cache.clear()
|
||||||
wrapper.hits = wrapper.misses = 0
|
use_count.clear()
|
||||||
|
wrapper.hits = wrapper.misses = 0
|
||||||
|
|
||||||
wrapper.hits = wrapper.misses = 0
|
wrapper.hits = wrapper.misses = 0
|
||||||
wrapper.clear = clear
|
wrapper.clear = clear
|
||||||
|
@ -161,9 +170,10 @@ def lru_cache(maxsize=100):
|
||||||
http://en.wikipedia.org/wiki/Cache_algorithms#Least_Recently_Used
|
http://en.wikipedia.org/wiki/Cache_algorithms#Least_Recently_Used
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def decorating_function(user_function):
|
def decorating_function(user_function, tuple=tuple, sorted=sorted, len=len):
|
||||||
cache = OrderedDict() # ordered least recent to most recent
|
cache = OrderedDict() # ordered least recent to most recent
|
||||||
kwd_mark = object() # separate positional and keyword args
|
kwd_mark = object() # separate positional and keyword args
|
||||||
|
lock = Lock()
|
||||||
|
|
||||||
@wraps(user_function)
|
@wraps(user_function)
|
||||||
def wrapper(*args, **kwds):
|
def wrapper(*args, **kwds):
|
||||||
|
@ -171,20 +181,25 @@ def lru_cache(maxsize=100):
|
||||||
if kwds:
|
if kwds:
|
||||||
key += (kwd_mark,) + tuple(sorted(kwds.items()))
|
key += (kwd_mark,) + tuple(sorted(kwds.items()))
|
||||||
try:
|
try:
|
||||||
result = cache.pop(key)
|
with lock:
|
||||||
wrapper.hits += 1
|
result = cache[key]
|
||||||
|
del cache[key]
|
||||||
|
cache[key] = result # record recent use of this key
|
||||||
|
wrapper.hits += 1
|
||||||
except KeyError:
|
except KeyError:
|
||||||
result = user_function(*args, **kwds)
|
result = user_function(*args, **kwds)
|
||||||
wrapper.misses += 1
|
with lock:
|
||||||
if len(cache) >= maxsize:
|
cache[key] = result # record recent use of this key
|
||||||
cache.popitem(0) # purge least recently used cache entry
|
wrapper.misses += 1
|
||||||
cache[key] = result # record recent use of this key
|
if len(cache) > maxsize:
|
||||||
|
cache.popitem(0) # purge least recently used cache entry
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def clear():
|
def clear():
|
||||||
"""Clear the cache and cache statistics"""
|
"""Clear the cache and cache statistics"""
|
||||||
cache.clear()
|
with lock:
|
||||||
wrapper.hits = wrapper.misses = 0
|
cache.clear()
|
||||||
|
wrapper.hits = wrapper.misses = 0
|
||||||
|
|
||||||
wrapper.hits = wrapper.misses = 0
|
wrapper.hits = wrapper.misses = 0
|
||||||
wrapper.clear = clear
|
wrapper.clear = clear
|
||||||
|
|
Loading…
Reference in New Issue