Skip to content

Commit

Permalink
threadsafe implementation (#14)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
Jutho authored Jul 18, 2019
1 parent d24a6d2 commit 8e64ae1
Show file tree
Hide file tree
Showing 8 changed files with 430 additions and 242 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ matrix:
env:
matrix:
- JULIA_NUM_THREADS=1
# - JULIA_NUM_THREADS=4
- JULIA_NUM_THREADS=4
#
## uncomment the following lines to override the default test script

Expand Down
5 changes: 3 additions & 2 deletions Project.toml
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
name = "LRUCache"
uuid = "8ac3fa9e-de4c-5943-b1dc-09c6b5f20637"
version = "0.3.0"
version = "1.0.0"

[compat]
julia = "1"

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

[targets]
test = ["Test"]
test = ["Test", "Random"]
42 changes: 32 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,25 @@
# LRUCache.jl

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

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

An LRU Cache is a useful associative data structure that has a set maximum
size. Once that size is reached, the least recently used items are removed
first.
An LRU Cache is a useful associative data structure (`AbstractDict` in Julia) that has a
set maximum size (as measured by number of elements or a custom size measure for items).
Once that size is reached, the least recently used items are removed first. A lock ensures
that data access does not lead to race conditions.

A particular use case of this package is to implement function memoization for functions
that can simultaneously be called from different threads.

## Installation
Install with the package manager via `]add LRUCache` or
```julia
using Pkg
Pkg.add("LRUCache")
```

## Interface

Expand All @@ -16,11 +29,16 @@ operations are shown below:
**Creation**

```julia
lru = LRU{K, V}(, maxsize = size)
lru = LRU{K, V}(, maxsize = size [, by = ...])
```

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

**Add an item to the cache**

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

Here, the maximal size is specified via a required keyword argument. Remember that the
maximal size is not necessarily the same as the maximal length, if a custom function was
specified using the keyword argument `by` in the construction of the LRU cache.

**Empty the cache**

```julia
Expand Down Expand Up @@ -72,15 +94,15 @@ end

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

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:
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:

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

## Example

Commonly, you may have some long running function that sometimes gets called
with the same parameters more than once. As such, it may benefit from cacheing
the results.
Commonly, you may have some long running function that sometimes gets called with the same
parameters more than once. As such, it may benefit from caching the results.

Here's our example, long running calculation:

Expand Down
176 changes: 87 additions & 89 deletions src/LRUCache.jl
Original file line number Diff line number Diff line change
@@ -1,126 +1,124 @@
module LRUCache

export LRU, @get!
include("cyclicorderedset.jl")
export LRU

include("list.jl")
using Base.Threads
using Base: Callable

# Default cache size
const __MAXCACHE__ = 100
_constone(x) = 1

# Default cache size
mutable struct LRU{K,V} <: AbstractDict{K,V}
ht::Dict{K, LRUNode{K, V}}
q::LRUList{K, V}
maxsize::Int

LRU{K, V}(; maxsize::Int) where {K, V} =
new{K, V}(Dict{K, V}(), LRUList{K, V}(), maxsize)
dict::Dict{K, Tuple{V, LinkedNode{K}, Int64}}
keyset::CyclicOrderedSet{K}
currentsize::Int64
maxsize::Int64
lock::SpinLock
by::Callable

LRU{K, V}(; maxsize::Int, by::Callable = _constone) where {K, V} =
new{K, V}(Dict{K, V}(), CyclicOrderedSet{K}(), 0, maxsize, SpinLock(), by)
end
LRU(; maxsize::Int) = LRU{Any,Any}(; maxsize = maxsize)

Base.@deprecate LRU(m::Int=__MAXCACHE__) LRU(; maxsize = m)
Base.@deprecate (LRU{K, V}(m::Int=__MAXCACHE__) where {K, V}) (LRU{K, V}(; maxsize = m))

Base.show(io::IO, lru::LRU{K, V}) where {K, V} = print(io,"LRU{$K, $V}($(lru.maxsize))")
Base.show(io::IO, lru::LRU{K, V}) where {K, V} =
print(io, "LRU{$K, $V}(; maxsize = $(lru.maxsize))")

Base.iterate(lru::LRU) = iterate(lru.ht)
Base.iterate(lru::LRU, state) = iterate(lru.ht, state)

Base.length(lru::LRU) = length(lru.q)
Base.isempty(lru::LRU) = isempty(lru.q)
Base.sizehint!(lru::LRU, n::Integer) = sizehint!(lru.ht, n)

Base.haskey(lru::LRU, key) = haskey(lru.ht, key)
Base.get(lru::LRU, key, default) = haskey(lru, key) ? lru[key] : default
Base.get(default::Base.Callable, lru::LRU, key) = haskey(lru, key) ? lru[key] : default()

macro get!(lru, key, default)
@warn "`@get! lru key default(args...)` is deprecated, use `get!(()->default(args...), lru, key)` or
```
get!(lru, key) do
default(args...)
end
```"
quote
if haskey($(esc(lru)), $(esc(key)))
value = $(esc(lru))[$(esc(key))]
else
value = $(esc(default))
$(esc(lru))[$(esc(key))] = value
end
value
end
end

function Base.get!(default::Base.Callable, lru::LRU{K, V}, key::K) where {K,V}
if haskey(lru, key)
return lru[key]
function Base.iterate(lru::LRU, state...)
next = iterate(lru.keyset, state...)
if next === nothing
return nothing
else
value = default()
lru[key] = value
return value
k, state = next
v, = lru.dict[k]
return k=>v, state
end
end

function Base.get!(lru::LRU{K,V}, key::K, default::V) where {K,V}
if haskey(lru, key)
return lru[key]
else
lru[key] = default
return default
end
Base.length(lru::LRU) = length(lru.keyset)
Base.isempty(lru::LRU) = isempty(lru.keyset)
function Base.sizehint!(lru::LRU, n::Integer)
sizehint!(lru.dict, n)
return lru
end

Base.haskey(lru::LRU, key) = haskey(lru.dict, key)
Base.get(lru::LRU, key, default) = haskey(lru, key) ? lru[key] : default
Base.get(default::Callable, lru::LRU, key) = haskey(lru, key) ? lru[key] : default()

Base.get!(default::Callable, lru::LRU, key) =
haskey(lru, key) ? lru[key] : (lru[key] = default())
Base.get!(lru::LRU, key, default) = haskey(lru, key) ? lru[key] : (lru[key] = default)

function Base.getindex(lru::LRU, key)
node = lru.ht[key]
move_to_front!(lru.q, node)
return node.v
lock(lru.lock)
v, n, s = lru.dict[key]
_move_to_front!(lru.keyset, n)
unlock(lru.lock)
return v
end

function Base.setindex!(lru::LRU{K, V}, v, key) where {K, V}
lock(lru.lock)
if haskey(lru, key)
item = lru.ht[key]
item.v = v
move_to_front!(lru.q, item)
elseif length(lru) == lru.maxsize
# At capacity. Roll the list so last el is now first, remove the old
# data, and update new data in place.
rotate!(lru.q)
item = first(lru.q)
delete!(lru.ht, item.k)
item.k = key
item.v = v
lru.ht[key] = item
_, n, s = lru.dict[key]
lru.currentsize -= s
s = lru.by(v)::Int
lru.currentsize += s
lru.dict[key] = (v, n, s)
_move_to_front!(lru.keyset, n)
else
item = LRUNode{K, V}(key, v)
pushfirst!(lru.q, item)
lru.ht[key] = item
n = LinkedNode{K}(key)
rotate!(_push!(lru.keyset, n))
s = lru.by(v)::Int
lru.currentsize += s
lru.dict[key] = (v, n, s)
end
while lru.currentsize > lru.maxsize
k = pop!(lru.keyset)
_, _, s = pop!(lru.dict, k)
lru.currentsize -= s
end
unlock(lru.lock)
return lru
end

import Base: resize!
Base.@deprecate resize!(lru::LRU, m::Int) resize!(lru; maxsize = m)

function resize!(lru::LRU; maxsize::Int)
maxsize < 0 && error("size must be a positive integer")
function Base.resize!(lru::LRU; maxsize::Integer = 0)
@assert 0 <= maxsize
lock(lru.lock)
lru.maxsize = maxsize
for i in 1:(length(lru) - lru.maxsize)
rm = pop!(lru.q)
delete!(lru.ht, rm.k)
while lru.currentsize > lru.maxsize
key = pop!(lru.keyset)
v, n, s = pop!(lru.dict, key)
lru.currentsize -= s
end
unlock(lru.lock)
return lru
end

function Base.delete!(lru::LRU, key)
item = lru.ht[key]
delete!(lru.q, item)
delete!(lru.ht, key)
lock(lru.lock)
v, n, s = pop!(lru.dict, key)
lru.currentsize -= s
_delete!(lru.keyset, n)
unlock(lru.lock)
return lru
end
function Base.pop!(lru::LRU, key)
lock(lru.lock)
v, n, s = pop!(lru.dict, key)
lru.currentsize -= s
_delete!(lru.keyset, n)
unlock(lru.lock)
return v
end

function Base.empty!(lru::LRU)
empty!(lru.ht)
empty!(lru.q)
lock(lru.lock)
lru.currentsize = 0
empty!(lru.dict)
empty!(lru.keyset)
unlock(lru.lock)
return lru
end

end # module
Loading

2 comments on commit 8e64ae1

@Jutho
Copy link
Collaborator Author

@Jutho Jutho commented on 8e64ae1 Jul 19, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JuliaRegistrator
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registration pull request created: JuliaRegistries/General/2133

After the above pull request is merged, it is recommended that a tag is created on this repository for the registered package version.

This will be done automatically if Julia TagBot is installed, or can be done manually through the github interface, or via:

git tag -a v1.0.0 -m "<description of version>" 8e64ae120eb42b6c1f5bf066200dc0c0ecb60163
git push origin v1.0.0

Please sign in to comment.