Skip to content

Commit 8e64ae1

Browse files
authored
threadsafe implementation (#14)
* threadsafe implementation, still run original tests * add new tests for multithreading, keep original tests as well * update README, bump version number to first major release
1 parent d24a6d2 commit 8e64ae1

File tree

8 files changed

+430
-242
lines changed

8 files changed

+430
-242
lines changed

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ matrix:
1818
env:
1919
matrix:
2020
- JULIA_NUM_THREADS=1
21-
# - JULIA_NUM_THREADS=4
21+
- JULIA_NUM_THREADS=4
2222
#
2323
## uncomment the following lines to override the default test script
2424

Project.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
name = "LRUCache"
22
uuid = "8ac3fa9e-de4c-5943-b1dc-09c6b5f20637"
3-
version = "0.3.0"
3+
version = "1.0.0"
44

55
[compat]
66
julia = "1"
77

88
[extras]
99
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
10+
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
1011

1112
[targets]
12-
test = ["Test"]
13+
test = ["Test", "Random"]

README.md

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,25 @@
11
# LRUCache.jl
22

33
[![Build Status](https://travis-ci.org/JuliaCollections/LRUCache.jl.svg)](https://travis-ci.org/JuliaCollections/LRUCache.jl)
4+
[![License](http://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat)](LICENSE.md)
5+
[![codecov.io](http://codecov.io/github/JuliaCollections/LRUCache.jl/coverage.svg?branch=master)](http://codecov.io/github/JuliaCollections/LRUCache.jl?branch=master)
46

5-
Provides an implementation of a Least Recently Used (LRU) Cache for Julia.
7+
Provides a thread-safe implementation of a Least Recently Used (LRU) Cache for Julia.
68

7-
An LRU Cache is a useful associative data structure that has a set maximum
8-
size. Once that size is reached, the least recently used items are removed
9-
first.
9+
An LRU Cache is a useful associative data structure (`AbstractDict` in Julia) that has a
10+
set maximum size (as measured by number of elements or a custom size measure for items).
11+
Once that size is reached, the least recently used items are removed first. A lock ensures
12+
that data access does not lead to race conditions.
13+
14+
A particular use case of this package is to implement function memoization for functions
15+
that can simultaneously be called from different threads.
16+
17+
## Installation
18+
Install with the package manager via `]add LRUCache` or
19+
```julia
20+
using Pkg
21+
Pkg.add("LRUCache")
22+
```
1023

1124
## Interface
1225

@@ -16,11 +29,16 @@ operations are shown below:
1629
**Creation**
1730

1831
```julia
19-
lru = LRU{K, V}(, maxsize = size)
32+
lru = LRU{K, V}(, maxsize = size [, by = ...])
2033
```
2134

2235
Create an LRU Cache with a maximum size (number of items) specified by the *required*
23-
keyword argument `maxsize`.
36+
keyword argument `maxsize`. Here, the size can be the number of elements (default), or the
37+
maximal total size of the values in the dictionary, as counted by an arbitrary user
38+
function (which should return a single value of type `Int`) specified with the keyword
39+
argument `by`. Sensible choices would for example be `by = sizeof` for e.g. values which
40+
are `Array`s of bitstypes, or `by = Base.summarysize` for values of some arbitrary user
41+
type.
2442

2543
**Add an item to the cache**
2644

@@ -42,6 +60,10 @@ lru[key]
4260
resize!(lru; maxsize = size)
4361
```
4462

63+
Here, the maximal size is specified via a required keyword argument. Remember that the
64+
maximal size is not necessarily the same as the maximal length, if a custom function was
65+
specified using the keyword argument `by` in the construction of the LRU cache.
66+
4567
**Empty the cache**
4668

4769
```julia
@@ -72,15 +94,15 @@ end
7294

7395
#### get(lru::LRU, key, default)
7496

75-
Returns the value stored in `lru` for `key` if present. If not, returns default without storing this value in `lru`. Also comes in the following form:
97+
Returns the value stored in `lru` for `key` if present. If not, returns default without
98+
storing this value in `lru`. Also comes in the following form:
7699

77100
#### get(default::Callable, lru::LRU, key)
78101

79102
## Example
80103

81-
Commonly, you may have some long running function that sometimes gets called
82-
with the same parameters more than once. As such, it may benefit from cacheing
83-
the results.
104+
Commonly, you may have some long running function that sometimes gets called with the same
105+
parameters more than once. As such, it may benefit from caching the results.
84106

85107
Here's our example, long running calculation:
86108

src/LRUCache.jl

Lines changed: 87 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -1,126 +1,124 @@
11
module LRUCache
22

3-
export LRU, @get!
3+
include("cyclicorderedset.jl")
4+
export LRU
45

5-
include("list.jl")
6+
using Base.Threads
7+
using Base: Callable
68

7-
# Default cache size
8-
const __MAXCACHE__ = 100
9+
_constone(x) = 1
910

11+
# Default cache size
1012
mutable struct LRU{K,V} <: AbstractDict{K,V}
11-
ht::Dict{K, LRUNode{K, V}}
12-
q::LRUList{K, V}
13-
maxsize::Int
14-
15-
LRU{K, V}(; maxsize::Int) where {K, V} =
16-
new{K, V}(Dict{K, V}(), LRUList{K, V}(), maxsize)
13+
dict::Dict{K, Tuple{V, LinkedNode{K}, Int64}}
14+
keyset::CyclicOrderedSet{K}
15+
currentsize::Int64
16+
maxsize::Int64
17+
lock::SpinLock
18+
by::Callable
19+
20+
LRU{K, V}(; maxsize::Int, by::Callable = _constone) where {K, V} =
21+
new{K, V}(Dict{K, V}(), CyclicOrderedSet{K}(), 0, maxsize, SpinLock(), by)
1722
end
18-
LRU(; maxsize::Int) = LRU{Any,Any}(; maxsize = maxsize)
19-
20-
Base.@deprecate LRU(m::Int=__MAXCACHE__) LRU(; maxsize = m)
21-
Base.@deprecate (LRU{K, V}(m::Int=__MAXCACHE__) where {K, V}) (LRU{K, V}(; maxsize = m))
2223

23-
Base.show(io::IO, lru::LRU{K, V}) where {K, V} = print(io,"LRU{$K, $V}($(lru.maxsize))")
24+
Base.show(io::IO, lru::LRU{K, V}) where {K, V} =
25+
print(io, "LRU{$K, $V}(; maxsize = $(lru.maxsize))")
2426

25-
Base.iterate(lru::LRU) = iterate(lru.ht)
26-
Base.iterate(lru::LRU, state) = iterate(lru.ht, state)
27-
28-
Base.length(lru::LRU) = length(lru.q)
29-
Base.isempty(lru::LRU) = isempty(lru.q)
30-
Base.sizehint!(lru::LRU, n::Integer) = sizehint!(lru.ht, n)
31-
32-
Base.haskey(lru::LRU, key) = haskey(lru.ht, key)
33-
Base.get(lru::LRU, key, default) = haskey(lru, key) ? lru[key] : default
34-
Base.get(default::Base.Callable, lru::LRU, key) = haskey(lru, key) ? lru[key] : default()
35-
36-
macro get!(lru, key, default)
37-
@warn "`@get! lru key default(args...)` is deprecated, use `get!(()->default(args...), lru, key)` or
38-
```
39-
get!(lru, key) do
40-
default(args...)
41-
end
42-
```"
43-
quote
44-
if haskey($(esc(lru)), $(esc(key)))
45-
value = $(esc(lru))[$(esc(key))]
46-
else
47-
value = $(esc(default))
48-
$(esc(lru))[$(esc(key))] = value
49-
end
50-
value
51-
end
52-
end
53-
54-
function Base.get!(default::Base.Callable, lru::LRU{K, V}, key::K) where {K,V}
55-
if haskey(lru, key)
56-
return lru[key]
27+
function Base.iterate(lru::LRU, state...)
28+
next = iterate(lru.keyset, state...)
29+
if next === nothing
30+
return nothing
5731
else
58-
value = default()
59-
lru[key] = value
60-
return value
32+
k, state = next
33+
v, = lru.dict[k]
34+
return k=>v, state
6135
end
6236
end
6337

64-
function Base.get!(lru::LRU{K,V}, key::K, default::V) where {K,V}
65-
if haskey(lru, key)
66-
return lru[key]
67-
else
68-
lru[key] = default
69-
return default
70-
end
38+
Base.length(lru::LRU) = length(lru.keyset)
39+
Base.isempty(lru::LRU) = isempty(lru.keyset)
40+
function Base.sizehint!(lru::LRU, n::Integer)
41+
sizehint!(lru.dict, n)
42+
return lru
7143
end
7244

45+
Base.haskey(lru::LRU, key) = haskey(lru.dict, key)
46+
Base.get(lru::LRU, key, default) = haskey(lru, key) ? lru[key] : default
47+
Base.get(default::Callable, lru::LRU, key) = haskey(lru, key) ? lru[key] : default()
48+
49+
Base.get!(default::Callable, lru::LRU, key) =
50+
haskey(lru, key) ? lru[key] : (lru[key] = default())
51+
Base.get!(lru::LRU, key, default) = haskey(lru, key) ? lru[key] : (lru[key] = default)
52+
7353
function Base.getindex(lru::LRU, key)
74-
node = lru.ht[key]
75-
move_to_front!(lru.q, node)
76-
return node.v
54+
lock(lru.lock)
55+
v, n, s = lru.dict[key]
56+
_move_to_front!(lru.keyset, n)
57+
unlock(lru.lock)
58+
return v
7759
end
78-
7960
function Base.setindex!(lru::LRU{K, V}, v, key) where {K, V}
61+
lock(lru.lock)
8062
if haskey(lru, key)
81-
item = lru.ht[key]
82-
item.v = v
83-
move_to_front!(lru.q, item)
84-
elseif length(lru) == lru.maxsize
85-
# At capacity. Roll the list so last el is now first, remove the old
86-
# data, and update new data in place.
87-
rotate!(lru.q)
88-
item = first(lru.q)
89-
delete!(lru.ht, item.k)
90-
item.k = key
91-
item.v = v
92-
lru.ht[key] = item
63+
_, n, s = lru.dict[key]
64+
lru.currentsize -= s
65+
s = lru.by(v)::Int
66+
lru.currentsize += s
67+
lru.dict[key] = (v, n, s)
68+
_move_to_front!(lru.keyset, n)
9369
else
94-
item = LRUNode{K, V}(key, v)
95-
pushfirst!(lru.q, item)
96-
lru.ht[key] = item
70+
n = LinkedNode{K}(key)
71+
rotate!(_push!(lru.keyset, n))
72+
s = lru.by(v)::Int
73+
lru.currentsize += s
74+
lru.dict[key] = (v, n, s)
75+
end
76+
while lru.currentsize > lru.maxsize
77+
k = pop!(lru.keyset)
78+
_, _, s = pop!(lru.dict, k)
79+
lru.currentsize -= s
9780
end
81+
unlock(lru.lock)
9882
return lru
9983
end
10084

101-
import Base: resize!
102-
Base.@deprecate resize!(lru::LRU, m::Int) resize!(lru; maxsize = m)
103-
104-
function resize!(lru::LRU; maxsize::Int)
105-
maxsize < 0 && error("size must be a positive integer")
85+
function Base.resize!(lru::LRU; maxsize::Integer = 0)
86+
@assert 0 <= maxsize
87+
lock(lru.lock)
10688
lru.maxsize = maxsize
107-
for i in 1:(length(lru) - lru.maxsize)
108-
rm = pop!(lru.q)
109-
delete!(lru.ht, rm.k)
89+
while lru.currentsize > lru.maxsize
90+
key = pop!(lru.keyset)
91+
v, n, s = pop!(lru.dict, key)
92+
lru.currentsize -= s
11093
end
94+
unlock(lru.lock)
11195
return lru
11296
end
11397

11498
function Base.delete!(lru::LRU, key)
115-
item = lru.ht[key]
116-
delete!(lru.q, item)
117-
delete!(lru.ht, key)
99+
lock(lru.lock)
100+
v, n, s = pop!(lru.dict, key)
101+
lru.currentsize -= s
102+
_delete!(lru.keyset, n)
103+
unlock(lru.lock)
118104
return lru
119105
end
106+
function Base.pop!(lru::LRU, key)
107+
lock(lru.lock)
108+
v, n, s = pop!(lru.dict, key)
109+
lru.currentsize -= s
110+
_delete!(lru.keyset, n)
111+
unlock(lru.lock)
112+
return v
113+
end
120114

121115
function Base.empty!(lru::LRU)
122-
empty!(lru.ht)
123-
empty!(lru.q)
116+
lock(lru.lock)
117+
lru.currentsize = 0
118+
empty!(lru.dict)
119+
empty!(lru.keyset)
120+
unlock(lru.lock)
121+
return lru
124122
end
125123

126124
end # module

0 commit comments

Comments
 (0)