thread_safe 0.1.1-java
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +21 -0
- data/Gemfile +4 -0
- data/LICENSE +144 -0
- data/README.md +34 -0
- data/Rakefile +36 -0
- data/examples/bench_cache.rb +35 -0
- data/ext/org/jruby/ext/thread_safe/JRubyCacheBackendLibrary.java +200 -0
- data/ext/org/jruby/ext/thread_safe/jsr166e/ConcurrentHashMapV8.java +3842 -0
- data/ext/org/jruby/ext/thread_safe/jsr166e/LongAdder.java +204 -0
- data/ext/org/jruby/ext/thread_safe/jsr166e/Striped64.java +342 -0
- data/ext/org/jruby/ext/thread_safe/jsr166y/ThreadLocalRandom.java +199 -0
- data/ext/thread_safe/JrubyCacheBackendService.java +15 -0
- data/lib/thread_safe.rb +65 -0
- data/lib/thread_safe/atomic_reference_cache_backend.rb +922 -0
- data/lib/thread_safe/cache.rb +137 -0
- data/lib/thread_safe/mri_cache_backend.rb +62 -0
- data/lib/thread_safe/non_concurrent_cache_backend.rb +133 -0
- data/lib/thread_safe/synchronized_cache_backend.rb +76 -0
- data/lib/thread_safe/synchronized_delegator.rb +35 -0
- data/lib/thread_safe/util.rb +16 -0
- data/lib/thread_safe/util/adder.rb +59 -0
- data/lib/thread_safe/util/atomic_reference.rb +12 -0
- data/lib/thread_safe/util/cheap_lockable.rb +105 -0
- data/lib/thread_safe/util/power_of_two_tuple.rb +26 -0
- data/lib/thread_safe/util/striped64.rb +226 -0
- data/lib/thread_safe/util/volatile.rb +62 -0
- data/lib/thread_safe/util/volatile_tuple.rb +46 -0
- data/lib/thread_safe/util/xor_shift_random.rb +39 -0
- data/lib/thread_safe/version.rb +3 -0
- data/test/test_array.rb +20 -0
- data/test/test_cache.rb +792 -0
- data/test/test_cache_loops.rb +453 -0
- data/test/test_hash.rb +20 -0
- data/test/test_helper.rb +73 -0
- data/test/test_synchronized_delegator.rb +42 -0
- data/thread_safe.gemspec +21 -0
- metadata +100 -0
@@ -0,0 +1,199 @@
|
|
1
|
+
/*
|
2
|
+
* Written by Doug Lea with assistance from members of JCP JSR-166
|
3
|
+
* Expert Group and released to the public domain, as explained at
|
4
|
+
* http://creativecommons.org/publicdomain/zero/1.0/
|
5
|
+
*/
|
6
|
+
|
7
|
+
// This is based on 1.16 version
|
8
|
+
|
9
|
+
package org.jruby.ext.thread_safe.jsr166y;
|
10
|
+
|
11
|
+
import java.util.Random;
|
12
|
+
|
13
|
+
/**
|
14
|
+
* A random number generator isolated to the current thread. Like the
|
15
|
+
* global {@link java.util.Random} generator used by the {@link
|
16
|
+
* java.lang.Math} class, a {@code ThreadLocalRandom} is initialized
|
17
|
+
* with an internally generated seed that may not otherwise be
|
18
|
+
* modified. When applicable, use of {@code ThreadLocalRandom} rather
|
19
|
+
* than shared {@code Random} objects in concurrent programs will
|
20
|
+
* typically encounter much less overhead and contention. Use of
|
21
|
+
* {@code ThreadLocalRandom} is particularly appropriate when multiple
|
22
|
+
* tasks (for example, each a {@link ForkJoinTask}) use random numbers
|
23
|
+
* in parallel in thread pools.
|
24
|
+
*
|
25
|
+
* <p>Usages of this class should typically be of the form:
|
26
|
+
* {@code ThreadLocalRandom.current().nextX(...)} (where
|
27
|
+
* {@code X} is {@code Int}, {@code Long}, etc).
|
28
|
+
* When all usages are of this form, it is never possible to
|
29
|
+
* accidently share a {@code ThreadLocalRandom} across multiple threads.
|
30
|
+
*
|
31
|
+
* <p>This class also provides additional commonly used bounded random
|
32
|
+
* generation methods.
|
33
|
+
*
|
34
|
+
* @since 1.7
|
35
|
+
* @author Doug Lea
|
36
|
+
*/
|
37
|
+
public class ThreadLocalRandom extends Random {
|
38
|
+
// same constants as Random, but must be redeclared because private
|
39
|
+
private static final long multiplier = 0x5DEECE66DL;
|
40
|
+
private static final long addend = 0xBL;
|
41
|
+
private static final long mask = (1L << 48) - 1;
|
42
|
+
|
43
|
+
/**
|
44
|
+
* The random seed. We can't use super.seed.
|
45
|
+
*/
|
46
|
+
private long rnd;
|
47
|
+
|
48
|
+
/**
|
49
|
+
* Initialization flag to permit calls to setSeed to succeed only
|
50
|
+
* while executing the Random constructor. We can't allow others
|
51
|
+
* since it would cause setting seed in one part of a program to
|
52
|
+
* unintentionally impact other usages by the thread.
|
53
|
+
*/
|
54
|
+
boolean initialized;
|
55
|
+
|
56
|
+
// Padding to help avoid memory contention among seed updates in
|
57
|
+
// different TLRs in the common case that they are located near
|
58
|
+
// each other.
|
59
|
+
private long pad0, pad1, pad2, pad3, pad4, pad5, pad6, pad7;
|
60
|
+
|
61
|
+
/**
|
62
|
+
* The actual ThreadLocal
|
63
|
+
*/
|
64
|
+
private static final ThreadLocal<ThreadLocalRandom> localRandom =
|
65
|
+
new ThreadLocal<ThreadLocalRandom>() {
|
66
|
+
protected ThreadLocalRandom initialValue() {
|
67
|
+
return new ThreadLocalRandom();
|
68
|
+
}
|
69
|
+
};
|
70
|
+
|
71
|
+
|
72
|
+
/**
|
73
|
+
* Constructor called only by localRandom.initialValue.
|
74
|
+
*/
|
75
|
+
ThreadLocalRandom() {
|
76
|
+
super();
|
77
|
+
initialized = true;
|
78
|
+
}
|
79
|
+
|
80
|
+
/**
|
81
|
+
* Returns the current thread's {@code ThreadLocalRandom}.
|
82
|
+
*
|
83
|
+
* @return the current thread's {@code ThreadLocalRandom}
|
84
|
+
*/
|
85
|
+
public static ThreadLocalRandom current() {
|
86
|
+
return localRandom.get();
|
87
|
+
}
|
88
|
+
|
89
|
+
/**
|
90
|
+
* Throws {@code UnsupportedOperationException}. Setting seeds in
|
91
|
+
* this generator is not supported.
|
92
|
+
*
|
93
|
+
* @throws UnsupportedOperationException always
|
94
|
+
*/
|
95
|
+
public void setSeed(long seed) {
|
96
|
+
if (initialized)
|
97
|
+
throw new UnsupportedOperationException();
|
98
|
+
rnd = (seed ^ multiplier) & mask;
|
99
|
+
}
|
100
|
+
|
101
|
+
protected int next(int bits) {
|
102
|
+
rnd = (rnd * multiplier + addend) & mask;
|
103
|
+
return (int) (rnd >>> (48-bits));
|
104
|
+
}
|
105
|
+
|
106
|
+
/**
|
107
|
+
* Returns a pseudorandom, uniformly distributed value between the
|
108
|
+
* given least value (inclusive) and bound (exclusive).
|
109
|
+
*
|
110
|
+
* @param least the least value returned
|
111
|
+
* @param bound the upper bound (exclusive)
|
112
|
+
* @throws IllegalArgumentException if least greater than or equal
|
113
|
+
* to bound
|
114
|
+
* @return the next value
|
115
|
+
*/
|
116
|
+
public int nextInt(int least, int bound) {
|
117
|
+
if (least >= bound)
|
118
|
+
throw new IllegalArgumentException();
|
119
|
+
return nextInt(bound - least) + least;
|
120
|
+
}
|
121
|
+
|
122
|
+
/**
|
123
|
+
* Returns a pseudorandom, uniformly distributed value
|
124
|
+
* between 0 (inclusive) and the specified value (exclusive).
|
125
|
+
*
|
126
|
+
* @param n the bound on the random number to be returned. Must be
|
127
|
+
* positive.
|
128
|
+
* @return the next value
|
129
|
+
* @throws IllegalArgumentException if n is not positive
|
130
|
+
*/
|
131
|
+
public long nextLong(long n) {
|
132
|
+
if (n <= 0)
|
133
|
+
throw new IllegalArgumentException("n must be positive");
|
134
|
+
// Divide n by two until small enough for nextInt. On each
|
135
|
+
// iteration (at most 31 of them but usually much less),
|
136
|
+
// randomly choose both whether to include high bit in result
|
137
|
+
// (offset) and whether to continue with the lower vs upper
|
138
|
+
// half (which makes a difference only if odd).
|
139
|
+
long offset = 0;
|
140
|
+
while (n >= Integer.MAX_VALUE) {
|
141
|
+
int bits = next(2);
|
142
|
+
long half = n >>> 1;
|
143
|
+
long nextn = ((bits & 2) == 0) ? half : n - half;
|
144
|
+
if ((bits & 1) == 0)
|
145
|
+
offset += n - nextn;
|
146
|
+
n = nextn;
|
147
|
+
}
|
148
|
+
return offset + nextInt((int) n);
|
149
|
+
}
|
150
|
+
|
151
|
+
/**
|
152
|
+
* Returns a pseudorandom, uniformly distributed value between the
|
153
|
+
* given least value (inclusive) and bound (exclusive).
|
154
|
+
*
|
155
|
+
* @param least the least value returned
|
156
|
+
* @param bound the upper bound (exclusive)
|
157
|
+
* @return the next value
|
158
|
+
* @throws IllegalArgumentException if least greater than or equal
|
159
|
+
* to bound
|
160
|
+
*/
|
161
|
+
public long nextLong(long least, long bound) {
|
162
|
+
if (least >= bound)
|
163
|
+
throw new IllegalArgumentException();
|
164
|
+
return nextLong(bound - least) + least;
|
165
|
+
}
|
166
|
+
|
167
|
+
/**
|
168
|
+
* Returns a pseudorandom, uniformly distributed {@code double} value
|
169
|
+
* between 0 (inclusive) and the specified value (exclusive).
|
170
|
+
*
|
171
|
+
* @param n the bound on the random number to be returned. Must be
|
172
|
+
* positive.
|
173
|
+
* @return the next value
|
174
|
+
* @throws IllegalArgumentException if n is not positive
|
175
|
+
*/
|
176
|
+
public double nextDouble(double n) {
|
177
|
+
if (n <= 0)
|
178
|
+
throw new IllegalArgumentException("n must be positive");
|
179
|
+
return nextDouble() * n;
|
180
|
+
}
|
181
|
+
|
182
|
+
/**
|
183
|
+
* Returns a pseudorandom, uniformly distributed value between the
|
184
|
+
* given least value (inclusive) and bound (exclusive).
|
185
|
+
*
|
186
|
+
* @param least the least value returned
|
187
|
+
* @param bound the upper bound (exclusive)
|
188
|
+
* @return the next value
|
189
|
+
* @throws IllegalArgumentException if least greater than or equal
|
190
|
+
* to bound
|
191
|
+
*/
|
192
|
+
public double nextDouble(double least, double bound) {
|
193
|
+
if (least >= bound)
|
194
|
+
throw new IllegalArgumentException();
|
195
|
+
return nextDouble() * (bound - least) + least;
|
196
|
+
}
|
197
|
+
|
198
|
+
private static final long serialVersionUID = -5851777807851030925L;
|
199
|
+
}
|
@@ -0,0 +1,15 @@
|
|
1
|
+
package thread_safe;
|
2
|
+
|
3
|
+
import java.io.IOException;
|
4
|
+
|
5
|
+
import org.jruby.Ruby;
|
6
|
+
import org.jruby.ext.thread_safe.JRubyCacheBackendLibrary;
|
7
|
+
import org.jruby.runtime.load.BasicLibraryService;
|
8
|
+
|
9
|
+
// can't name this JRubyCacheBackendService or else JRuby doesn't pick this up
|
10
|
+
public class JrubyCacheBackendService implements BasicLibraryService {
|
11
|
+
public boolean basicLoad(final Ruby runtime) throws IOException {
|
12
|
+
new JRubyCacheBackendLibrary().load(runtime, false);
|
13
|
+
return true;
|
14
|
+
}
|
15
|
+
}
|
data/lib/thread_safe.rb
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'thread_safe/version'
|
2
|
+
require 'thread_safe/synchronized_delegator'
|
3
|
+
|
4
|
+
module ThreadSafe
|
5
|
+
autoload :Cache, 'thread_safe/cache'
|
6
|
+
autoload :Util, 'thread_safe/util'
|
7
|
+
|
8
|
+
# Various classes within allows for +nil+ values to be stored, so a special +NULL+ token is required to indicate the "nil-ness".
|
9
|
+
NULL = Object.new
|
10
|
+
|
11
|
+
if defined?(JRUBY_VERSION)
|
12
|
+
require 'jruby/synchronized'
|
13
|
+
|
14
|
+
# A thread-safe subclass of Array. This version locks
|
15
|
+
# against the object itself for every method call,
|
16
|
+
# ensuring only one thread can be reading or writing
|
17
|
+
# at a time. This includes iteration methods like
|
18
|
+
# #each.
|
19
|
+
class Array < ::Array
|
20
|
+
include JRuby::Synchronized
|
21
|
+
end
|
22
|
+
|
23
|
+
# A thread-safe subclass of Hash. This version locks
|
24
|
+
# against the object itself for every method call,
|
25
|
+
# ensuring only one thread can be reading or writing
|
26
|
+
# at a time. This includes iteration methods like
|
27
|
+
# #each.
|
28
|
+
class Hash < ::Hash
|
29
|
+
include JRuby::Synchronized
|
30
|
+
end
|
31
|
+
elsif defined?(RUBY_ENGINE) && RUBY_ENGINE == 'ruby'
|
32
|
+
# Because MRI never runs code in parallel, the existing
|
33
|
+
# non-thread-safe structures should usually work fine.
|
34
|
+
Array = ::Array
|
35
|
+
Hash = ::Hash
|
36
|
+
elsif defined?(RUBY_ENGINE) && RUBY_ENGINE == 'rbx'
|
37
|
+
require 'monitor'
|
38
|
+
|
39
|
+
class Hash < ::Hash; end
|
40
|
+
class Array < ::Array; end
|
41
|
+
|
42
|
+
[Hash, Array].each do |klass|
|
43
|
+
klass.class_eval do
|
44
|
+
private
|
45
|
+
def _mon_initialize
|
46
|
+
@_monitor = Monitor.new unless @_monitor # avoid double initialisation
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.allocate
|
50
|
+
obj = super
|
51
|
+
obj.send(:_mon_initialize)
|
52
|
+
obj
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
klass.superclass.instance_methods(false).each do |method|
|
57
|
+
klass.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
|
58
|
+
def #{method}(*args)
|
59
|
+
@_monitor.synchronize { super }
|
60
|
+
end
|
61
|
+
RUBY_EVAL
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,922 @@
|
|
1
|
+
module ThreadSafe
|
2
|
+
# A Ruby port of the Doug Lea's jsr166e.ConcurrentHashMapV8 class version 1.59 available in public domain.
|
3
|
+
# Original source code available here: http://gee.cs.oswego.edu/cgi-bin/viewcvs.cgi/jsr166/src/jsr166e/ConcurrentHashMapV8.java?revision=1.59
|
4
|
+
#
|
5
|
+
# The Ruby port skips out the +TreeBin+ (red-black trees for use in bins
|
6
|
+
# whose size exceeds a threshold).
|
7
|
+
#
|
8
|
+
# A hash table supporting full concurrency of retrievals and
|
9
|
+
# high expected concurrency for updates. However, even though all
|
10
|
+
# operations are thread-safe, retrieval operations do _not_ entail locking,
|
11
|
+
# and there is _not_ any support for locking the entire table
|
12
|
+
# in a way that prevents all access.
|
13
|
+
#
|
14
|
+
# Retrieval operations generally do not block, so may overlap with
|
15
|
+
# update operations. Retrievals reflect the results of the most
|
16
|
+
# recently _completed_ update operations holding upon their
|
17
|
+
# onset. (More formally, an update operation for a given key bears a
|
18
|
+
# _happens-before_ relation with any (non +nil+) retrieval for
|
19
|
+
# that key reporting the updated value.) For aggregate operations
|
20
|
+
# such as +clear()+, concurrent retrievals may reflect insertion or removal
|
21
|
+
# of only some entries. Similarly, the +each_pair+ iterator yields elements
|
22
|
+
# reflecting the state of the hash table at some point at or since
|
23
|
+
# the start of the +each_pair+. Bear in mind that the results of
|
24
|
+
# aggregate status methods including +size()+ and +empty?+} are typically
|
25
|
+
# useful only when a map is not undergoing concurrent updates in other
|
26
|
+
# threads. Otherwise the results of these methods reflect transient
|
27
|
+
# states that may be adequate for monitoring or estimation purposes, but not
|
28
|
+
# for program control.
|
29
|
+
#
|
30
|
+
# The table is dynamically expanded when there are too many
|
31
|
+
# collisions (i.e., keys that have distinct hash codes but fall into
|
32
|
+
# the same slot modulo the table size), with the expected average
|
33
|
+
# effect of maintaining roughly two bins per mapping (corresponding
|
34
|
+
# to a 0.75 load factor threshold for resizing). There may be much
|
35
|
+
# variance around this average as mappings are added and removed, but
|
36
|
+
# overall, this maintains a commonly accepted time/space tradeoff for
|
37
|
+
# hash tables. However, resizing this or any other kind of hash
|
38
|
+
# table may be a relatively slow operation. When possible, it is a
|
39
|
+
# good idea to provide a size estimate as an optional :initial_capacity
|
40
|
+
# initializer argument. An additional optional :load_factor constructor
|
41
|
+
# argument provides a further means of customizing initial table capacity
|
42
|
+
# by specifying the table density to be used in calculating the amount of
|
43
|
+
# space to allocate for the given number of elements. Note that using
|
44
|
+
# many keys with exactly the same +hash+ is a sure way to slow down
|
45
|
+
# performance of any hash table.
|
46
|
+
#
|
47
|
+
# == Design overview
|
48
|
+
#
|
49
|
+
# The primary design goal of this hash table is to maintain
|
50
|
+
# concurrent readability (typically method +[]+, but also
|
51
|
+
# iteration and related methods) while minimizing update
|
52
|
+
# contention. Secondary goals are to keep space consumption about
|
53
|
+
# the same or better than plain +Hash+, and to support high
|
54
|
+
# initial insertion rates on an empty table by many threads.
|
55
|
+
#
|
56
|
+
# Each key-value mapping is held in a +Node+. The validation-based
|
57
|
+
# approach explained below leads to a lot of code sprawl because
|
58
|
+
# retry-control precludes factoring into smaller methods.
|
59
|
+
#
|
60
|
+
# The table is lazily initialized to a power-of-two size upon the
|
61
|
+
# first insertion. Each bin in the table normally contains a
|
62
|
+
# list of +Node+s (most often, the list has only zero or one +Node+).
|
63
|
+
# Table accesses require volatile/atomic reads, writes, and
|
64
|
+
# CASes. The lists of nodes within bins are always accurately traversable
|
65
|
+
# under volatile reads, so long as lookups check hash code
|
66
|
+
# and non-nullness of value before checking key equality.
|
67
|
+
#
|
68
|
+
# We use the top two bits of +Node+ hash fields for control
|
69
|
+
# purposes -- they are available anyway because of addressing
|
70
|
+
# constraints. As explained further below, these top bits are
|
71
|
+
# used as follows:
|
72
|
+
# 00 - Normal
|
73
|
+
# 01 - Locked
|
74
|
+
# 11 - Locked and may have a thread waiting for lock
|
75
|
+
# 10 - +Node+ is a forwarding node
|
76
|
+
#
|
77
|
+
# The lower 28 bits of each +Node+'s hash field contain a
|
78
|
+
# the key's hash code, except for forwarding nodes, for which
|
79
|
+
# the lower bits are zero (and so always have hash field == +MOVED+).
|
80
|
+
#
|
81
|
+
# Insertion (via +[]=+ or its variants) of the first node in an
|
82
|
+
# empty bin is performed by just CASing it to the bin. This is
|
83
|
+
# by far the most common case for put operations under most
|
84
|
+
# key/hash distributions. Other update operations (insert,
|
85
|
+
# delete, and replace) require locks. We do not want to waste
|
86
|
+
# the space required to associate a distinct lock object with
|
87
|
+
# each bin, so instead use the first node of a bin list itself as
|
88
|
+
# a lock. Blocking support for these locks relies +Util::CheapLockable.
|
89
|
+
# However, we also need a +try_lock+ construction, so we overlay
|
90
|
+
# these by using bits of the +Node+ hash field for lock control (see above),
|
91
|
+
# and so normally use builtin monitors only for blocking and signalling using
|
92
|
+
# +cheap_wait+/+cheap_broadcast+ constructions. See +Node#try_await_lock+.
|
93
|
+
#
|
94
|
+
# Using the first node of a list as a lock does not by itself
|
95
|
+
# suffice though: When a node is locked, any update must first
|
96
|
+
# validate that it is still the first node after locking it, and
|
97
|
+
# retry if not. Because new nodes are always appended to lists,
|
98
|
+
# once a node is first in a bin, it remains first until deleted
|
99
|
+
# or the bin becomes invalidated (upon resizing). However,
|
100
|
+
# operations that only conditionally update may inspect nodes
|
101
|
+
# until the point of update. This is a converse of sorts to the
|
102
|
+
# lazy locking technique described by Herlihy & Shavit.
|
103
|
+
#
|
104
|
+
# The main disadvantage of per-bin locks is that other update
|
105
|
+
# operations on other nodes in a bin list protected by the same
|
106
|
+
# lock can stall, for example when user +eql?+ or mapping
|
107
|
+
# functions take a long time. However, statistically, under
|
108
|
+
# random hash codes, this is not a common problem. Ideally, the
|
109
|
+
# frequency of nodes in bins follows a Poisson distribution
|
110
|
+
# (http://en.wikipedia.org/wiki/Poisson_distribution) with a
|
111
|
+
# parameter of about 0.5 on average, given the resizing threshold
|
112
|
+
# of 0.75, although with a large variance because of resizing
|
113
|
+
# granularity. Ignoring variance, the expected occurrences of
|
114
|
+
# list size k are (exp(-0.5) * pow(0.5, k) / factorial(k)). The
|
115
|
+
# first values are:
|
116
|
+
#
|
117
|
+
# 0: 0.60653066
|
118
|
+
# 1: 0.30326533
|
119
|
+
# 2: 0.07581633
|
120
|
+
# 3: 0.01263606
|
121
|
+
# 4: 0.00157952
|
122
|
+
# 5: 0.00015795
|
123
|
+
# 6: 0.00001316
|
124
|
+
# 7: 0.00000094
|
125
|
+
# 8: 0.00000006
|
126
|
+
# more: less than 1 in ten million
|
127
|
+
#
|
128
|
+
# Lock contention probability for two threads accessing distinct
|
129
|
+
# elements is roughly 1 / (8 * #elements) under random hashes.
|
130
|
+
#
|
131
|
+
# The table is resized when occupancy exceeds a percentage
|
132
|
+
# threshold (nominally, 0.75, but see below). Only a single
|
133
|
+
# thread performs the resize (using field +size_control+, to arrange
|
134
|
+
# exclusion), but the table otherwise remains usable for reads
|
135
|
+
# and updates. Resizing proceeds by transferring bins, one by
|
136
|
+
# one, from the table to the next table. Because we are using
|
137
|
+
# power-of-two expansion, the elements from each bin must either
|
138
|
+
# stay at same index, or move with a power of two offset. We
|
139
|
+
# eliminate unnecessary node creation by catching cases where old
|
140
|
+
# nodes can be reused because their next fields won't change. On
|
141
|
+
# average, only about one-sixth of them need cloning when a table
|
142
|
+
# doubles. The nodes they replace will be garbage collectable as
|
143
|
+
# soon as they are no longer referenced by any reader thread that
|
144
|
+
# may be in the midst of concurrently traversing table. Upon
|
145
|
+
# transfer, the old table bin contains only a special forwarding
|
146
|
+
# node (with hash field +MOVED+) that contains the next table as
|
147
|
+
# its key. On encountering a forwarding node, access and update
|
148
|
+
# operations restart, using the new table.
|
149
|
+
#
|
150
|
+
# Each bin transfer requires its bin lock. However, unlike other
|
151
|
+
# cases, a transfer can skip a bin if it fails to acquire its
|
152
|
+
# lock, and revisit it later. Method +rebuild+ maintains a buffer of
|
153
|
+
# TRANSFER_BUFFER_SIZE bins that have been skipped because of failure
|
154
|
+
# to acquire a lock, and blocks only if none are available
|
155
|
+
# (i.e., only very rarely). The transfer operation must also ensure
|
156
|
+
# that all accessible bins in both the old and new table are usable by
|
157
|
+
# any traversal. When there are no lock acquisition failures, this is
|
158
|
+
# arranged simply by proceeding from the last bin (+table.size - 1+) up
|
159
|
+
# towards the first. Upon seeing a forwarding node, traversals arrange
|
160
|
+
# to move to the new table without revisiting nodes. However, when any
|
161
|
+
# node is skipped during a transfer, all earlier table bins may have
|
162
|
+
# become visible, so are initialized with a reverse-forwarding node back
|
163
|
+
# to the old table until the new ones are established. (This
|
164
|
+
# sometimes requires transiently locking a forwarding node, which
|
165
|
+
# is possible under the above encoding.) These more expensive
|
166
|
+
# mechanics trigger only when necessary.
|
167
|
+
#
|
168
|
+
# The traversal scheme also applies to partial traversals of
|
169
|
+
# ranges of bins (via an alternate Traverser constructor)
|
170
|
+
# to support partitioned aggregate operations. Also, read-only
|
171
|
+
# operations give up if ever forwarded to a null table, which
|
172
|
+
# provides support for shutdown-style clearing, which is also not
|
173
|
+
# currently implemented.
|
174
|
+
#
|
175
|
+
# Lazy table initialization minimizes footprint until first use.
|
176
|
+
#
|
177
|
+
# The element count is maintained using a +ThreadSafe::Util::Adder+,
|
178
|
+
# which avoids contention on updates but can encounter cache thrashing
|
179
|
+
# if read too frequently during concurrent access. To avoid reading so
|
180
|
+
# often, resizing is attempted either when a bin lock is
|
181
|
+
# contended, or upon adding to a bin already holding two or more
|
182
|
+
# nodes (checked before adding in the +x_if_absent+ methods, after
|
183
|
+
# adding in others). Under uniform hash distributions, the
|
184
|
+
# probability of this occurring at threshold is around 13%,
|
185
|
+
# meaning that only about 1 in 8 puts check threshold (and after
|
186
|
+
# resizing, many fewer do so). But this approximation has high
|
187
|
+
# variance for small table sizes, so we check on any collision
|
188
|
+
# for sizes <= 64. The bulk putAll operation further reduces
|
189
|
+
# contention by only committing count updates upon these size
|
190
|
+
# checks.
|
191
|
+
class AtomicReferenceCacheBackend
|
192
|
+
class Table < Util::PowerOfTwoTuple
|
193
|
+
def cas_new_node(i, hash, key, value)
|
194
|
+
cas(i, nil, Node.new(hash, key, value))
|
195
|
+
end
|
196
|
+
|
197
|
+
def try_to_cas_in_computed(i, hash, key)
|
198
|
+
succeeded = false
|
199
|
+
new_value = nil
|
200
|
+
new_node = Node.new(locked_hash = hash | LOCKED, key, NULL)
|
201
|
+
if cas(i, nil, new_node)
|
202
|
+
begin
|
203
|
+
if NULL == (new_value = yield(NULL))
|
204
|
+
was_null = true
|
205
|
+
else
|
206
|
+
new_node.value = new_value
|
207
|
+
end
|
208
|
+
succeeded = true
|
209
|
+
ensure
|
210
|
+
volatile_set(i, nil) if !succeeded || was_null
|
211
|
+
new_node.unlock_via_hash(locked_hash, hash)
|
212
|
+
end
|
213
|
+
end
|
214
|
+
return succeeded, new_value
|
215
|
+
end
|
216
|
+
|
217
|
+
def try_lock_via_hash(i, node, node_hash)
|
218
|
+
node.try_lock_via_hash(node_hash) do
|
219
|
+
yield if volatile_get(i) == node
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
def delete_node_at(i, node, predecessor_node)
|
224
|
+
if predecessor_node
|
225
|
+
predecessor_node.next = node.next
|
226
|
+
else
|
227
|
+
volatile_set(i, node.next)
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
# Key-value entry. Nodes with a hash field of +MOVED+ are special,
|
233
|
+
# and do not contain user keys or values. Otherwise, keys are never +nil+,
|
234
|
+
# and +NULL+ +value+ fields indicate that a node is in the process
|
235
|
+
# of being deleted or created. For purposes of read-only access, a key may be read
|
236
|
+
# before a value, but can only be used after checking value to be +!= NULL+.
|
237
|
+
class Node
|
238
|
+
extend Util::Volatile
|
239
|
+
attr_volatile :hash, :value, :next
|
240
|
+
|
241
|
+
include Util::CheapLockable
|
242
|
+
|
243
|
+
bit_shift = Util::FIXNUM_BIT_SIZE - 2 # need 2 bits for ourselves
|
244
|
+
# Encodings for special uses of Node hash fields. See above for explanation.
|
245
|
+
MOVED = ('10' << ('0' * bit_shift)).to_i(2) # hash field for forwarding nodes
|
246
|
+
LOCKED = ('01' << ('0' * bit_shift)).to_i(2) # set/tested only as a bit
|
247
|
+
WAITING = ('11' << ('0' * bit_shift)).to_i(2) # both bits set/tested together
|
248
|
+
HASH_BITS = ('00' << ('1' * bit_shift)).to_i(2) # usable bits of normal node hash
|
249
|
+
|
250
|
+
SPIN_LOCK_ATTEMPTS = Util::CPU_COUNT > 1 ? Util::CPU_COUNT * 2 : 0
|
251
|
+
|
252
|
+
attr_reader :key
|
253
|
+
|
254
|
+
def initialize(hash, key, value, next_node = nil)
|
255
|
+
super()
|
256
|
+
@key = key
|
257
|
+
self.lazy_set_hash(hash)
|
258
|
+
self.lazy_set_value(value)
|
259
|
+
self.next = next_node
|
260
|
+
end
|
261
|
+
|
262
|
+
# Spins a while if +LOCKED+ bit set and this node is the first
|
263
|
+
# of its bin, and then sets +WAITING+ bits on hash field and
|
264
|
+
# blocks (once) if they are still set. It is OK for this
|
265
|
+
# method to return even if lock is not available upon exit,
|
266
|
+
# which enables these simple single-wait mechanics.
|
267
|
+
#
|
268
|
+
# The corresponding signalling operation is performed within
|
269
|
+
# callers: Upon detecting that +WAITING+ has been set when
|
270
|
+
# unlocking lock (via a failed CAS from non-waiting +LOCKED+
|
271
|
+
# state), unlockers acquire the +cheap_synchronize+ lock and
|
272
|
+
# perform a +cheap_broadcast+.
|
273
|
+
def try_await_lock(table, i)
|
274
|
+
if table && i >= 0 && i < table.size # bounds check, TODO: why are we bounds checking?
|
275
|
+
spins = SPIN_LOCK_ATTEMPTS
|
276
|
+
randomizer = base_randomizer = Util::XorShiftRandom.get
|
277
|
+
while equal?(table.volatile_get(i)) && self.class.locked_hash?(my_hash = hash)
|
278
|
+
if spins >= 0
|
279
|
+
if (randomizer = (randomizer >> 1)).even? # spin at random
|
280
|
+
if (spins -= 1) == 0
|
281
|
+
Thread.pass # yield before blocking
|
282
|
+
else
|
283
|
+
randomizer = base_randomizer = Util::XorShiftRandom.xorshift(base_randomizer) if randomizer.zero?
|
284
|
+
end
|
285
|
+
end
|
286
|
+
elsif cas_hash(my_hash, my_hash | WAITING)
|
287
|
+
force_aquire_lock(table, i)
|
288
|
+
break
|
289
|
+
end
|
290
|
+
end
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
def key?(key)
|
295
|
+
@key.eql?(key)
|
296
|
+
end
|
297
|
+
|
298
|
+
def matches?(key, hash)
|
299
|
+
pure_hash == hash && key?(key)
|
300
|
+
end
|
301
|
+
|
302
|
+
def pure_hash
|
303
|
+
hash & HASH_BITS
|
304
|
+
end
|
305
|
+
|
306
|
+
def try_lock_via_hash(node_hash = hash)
|
307
|
+
if cas_hash(node_hash, locked_hash = node_hash | LOCKED)
|
308
|
+
begin
|
309
|
+
yield
|
310
|
+
ensure
|
311
|
+
unlock_via_hash(locked_hash, node_hash)
|
312
|
+
end
|
313
|
+
end
|
314
|
+
end
|
315
|
+
|
316
|
+
def locked?
|
317
|
+
self.class.locked_hash?(hash)
|
318
|
+
end
|
319
|
+
|
320
|
+
def unlock_via_hash(locked_hash, node_hash)
|
321
|
+
unless cas_hash(locked_hash, node_hash)
|
322
|
+
self.hash = node_hash
|
323
|
+
cheap_synchronize { cheap_broadcast }
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
327
|
+
private
|
328
|
+
def force_aquire_lock(table, i)
|
329
|
+
cheap_synchronize do
|
330
|
+
if equal?(table.volatile_get(i)) && (hash & WAITING) == WAITING
|
331
|
+
cheap_wait
|
332
|
+
else
|
333
|
+
cheap_broadcast # possibly won race vs signaller
|
334
|
+
end
|
335
|
+
end
|
336
|
+
end
|
337
|
+
|
338
|
+
class << self
|
339
|
+
def locked_hash?(hash)
|
340
|
+
(hash & LOCKED) != 0
|
341
|
+
end
|
342
|
+
end
|
343
|
+
end
|
344
|
+
|
345
|
+
# shorthands
|
346
|
+
MOVED = Node::MOVED
|
347
|
+
LOCKED = Node::LOCKED
|
348
|
+
WAITING = Node::WAITING
|
349
|
+
HASH_BITS = Node::HASH_BITS
|
350
|
+
|
351
|
+
NOW_RESIZING = -1
|
352
|
+
DEFAULT_CAPACITY = 16
|
353
|
+
MAX_CAPACITY = Util::MAX_INT
|
354
|
+
|
355
|
+
# The buffer size for skipped bins during transfers. The
|
356
|
+
# value is arbitrary but should be large enough to avoid
|
357
|
+
# most locking stalls during resizes.
|
358
|
+
TRANSFER_BUFFER_SIZE = 32
|
359
|
+
|
360
|
+
extend Util::Volatile
|
361
|
+
attr_volatile :table, # The array of bins. Lazily initialized upon first insertion. Size is always a power of two.
|
362
|
+
|
363
|
+
# Table initialization and resizing control. When negative, the
|
364
|
+
# table is being initialized or resized. Otherwise, when table is
|
365
|
+
# null, holds the initial table size to use upon creation, or 0
|
366
|
+
# for default. After initialization, holds the next element count
|
367
|
+
# value upon which to resize the table.
|
368
|
+
:size_control
|
369
|
+
|
370
|
+
def initialize(options = nil)
|
371
|
+
super()
|
372
|
+
@counter = Util::Adder.new
|
373
|
+
initial_capacity = options && options[:initial_capacity] || DEFAULT_CAPACITY
|
374
|
+
self.size_control = (capacity = table_size_for(initial_capacity)) > MAX_CAPACITY ? MAX_CAPACITY : capacity
|
375
|
+
end
|
376
|
+
|
377
|
+
def get_or_default(key, else_value = nil)
|
378
|
+
hash = key_hash(key)
|
379
|
+
current_table = table
|
380
|
+
while current_table
|
381
|
+
node = current_table.volatile_get_by_hash(hash)
|
382
|
+
current_table =
|
383
|
+
while node
|
384
|
+
if (node_hash = node.hash) == MOVED
|
385
|
+
break node.key
|
386
|
+
elsif (node_hash & HASH_BITS) == hash && node.key?(key) && NULL != (value = node.value)
|
387
|
+
return value
|
388
|
+
end
|
389
|
+
node = node.next
|
390
|
+
end
|
391
|
+
end
|
392
|
+
else_value
|
393
|
+
end
|
394
|
+
|
395
|
+
def [](key)
|
396
|
+
get_or_default(key)
|
397
|
+
end
|
398
|
+
|
399
|
+
def key?(key)
|
400
|
+
get_or_default(key, NULL) != NULL
|
401
|
+
end
|
402
|
+
|
403
|
+
def []=(key, value)
|
404
|
+
get_and_set(key, value)
|
405
|
+
value
|
406
|
+
end
|
407
|
+
|
408
|
+
def compute_if_absent(key)
|
409
|
+
hash = key_hash(key)
|
410
|
+
current_table = table || initialize_table
|
411
|
+
while true
|
412
|
+
if !(node = current_table.volatile_get(i = current_table.hash_to_index(hash)))
|
413
|
+
succeeded, new_value = current_table.try_to_cas_in_computed(i, hash, key) { yield }
|
414
|
+
if succeeded
|
415
|
+
increment_size
|
416
|
+
return new_value
|
417
|
+
end
|
418
|
+
elsif (node_hash = node.hash) == MOVED
|
419
|
+
current_table = node.key
|
420
|
+
elsif NULL != (current_value = find_value_in_node_list(node, key, hash, node_hash & HASH_BITS))
|
421
|
+
return current_value
|
422
|
+
elsif Node.locked_hash?(node_hash)
|
423
|
+
try_await_lock(current_table, i, node)
|
424
|
+
else
|
425
|
+
succeeded, value = attempt_internal_compute_if_absent(key, hash, current_table, i, node, node_hash) { yield }
|
426
|
+
return value if succeeded
|
427
|
+
end
|
428
|
+
end
|
429
|
+
end
|
430
|
+
|
431
|
+
def compute_if_present(key)
|
432
|
+
new_value = nil
|
433
|
+
internal_replace(key) do |old_value|
|
434
|
+
if (new_value = yield(NULL == old_value ? nil : old_value)).nil?
|
435
|
+
NULL
|
436
|
+
else
|
437
|
+
new_value
|
438
|
+
end
|
439
|
+
end
|
440
|
+
new_value
|
441
|
+
end
|
442
|
+
|
443
|
+
def compute(key)
|
444
|
+
internal_compute(key) do |old_value|
|
445
|
+
if (new_value = yield(NULL == old_value ? nil : old_value)).nil?
|
446
|
+
NULL
|
447
|
+
else
|
448
|
+
new_value
|
449
|
+
end
|
450
|
+
end
|
451
|
+
end
|
452
|
+
|
453
|
+
def merge_pair(key, value)
|
454
|
+
internal_compute(key) do |old_value|
|
455
|
+
if NULL == old_value || !(value = yield(old_value)).nil?
|
456
|
+
value
|
457
|
+
else
|
458
|
+
NULL
|
459
|
+
end
|
460
|
+
end
|
461
|
+
end
|
462
|
+
|
463
|
+
def replace_pair(key, old_value, new_value)
|
464
|
+
NULL != internal_replace(key, old_value) { new_value }
|
465
|
+
end
|
466
|
+
|
467
|
+
def replace_if_exists(key, new_value)
|
468
|
+
if (result = internal_replace(key) { new_value }) && NULL != result
|
469
|
+
result
|
470
|
+
end
|
471
|
+
end
|
472
|
+
|
473
|
+
def get_and_set(key, value) # internalPut in the original CHMV8
|
474
|
+
hash = key_hash(key)
|
475
|
+
current_table = table || initialize_table
|
476
|
+
while true
|
477
|
+
if !(node = current_table.volatile_get(i = current_table.hash_to_index(hash)))
|
478
|
+
if current_table.cas_new_node(i, hash, key, value)
|
479
|
+
increment_size
|
480
|
+
break
|
481
|
+
end
|
482
|
+
elsif (node_hash = node.hash) == MOVED
|
483
|
+
current_table = node.key
|
484
|
+
elsif Node.locked_hash?(node_hash)
|
485
|
+
try_await_lock(current_table, i, node)
|
486
|
+
else
|
487
|
+
succeeded, old_value = attempt_get_and_set(key, value, hash, current_table, i, node, node_hash)
|
488
|
+
break old_value if succeeded
|
489
|
+
end
|
490
|
+
end
|
491
|
+
end
|
492
|
+
|
493
|
+
def delete(key)
|
494
|
+
replace_if_exists(key, NULL)
|
495
|
+
end
|
496
|
+
|
497
|
+
def delete_pair(key, value)
|
498
|
+
result = internal_replace(key, value) { NULL }
|
499
|
+
if result && NULL != result
|
500
|
+
!!result
|
501
|
+
else
|
502
|
+
false
|
503
|
+
end
|
504
|
+
end
|
505
|
+
|
506
|
+
def each_pair
|
507
|
+
return self unless current_table = table
|
508
|
+
current_table_size = base_size = current_table.size
|
509
|
+
i = base_index = 0
|
510
|
+
while base_index < base_size
|
511
|
+
if node = current_table.volatile_get(i)
|
512
|
+
if node.hash == MOVED
|
513
|
+
current_table = node.key
|
514
|
+
current_table_size = current_table.size
|
515
|
+
else
|
516
|
+
begin
|
517
|
+
if NULL != (value = node.value) # skip deleted or special nodes
|
518
|
+
yield node.key, value
|
519
|
+
end
|
520
|
+
end while node = node.next
|
521
|
+
end
|
522
|
+
end
|
523
|
+
|
524
|
+
if (i_with_base = i + base_size) < current_table_size
|
525
|
+
i = i_with_base # visit upper slots if present
|
526
|
+
else
|
527
|
+
i = base_index += 1
|
528
|
+
end
|
529
|
+
end
|
530
|
+
self
|
531
|
+
end
|
532
|
+
|
533
|
+
def size
|
534
|
+
(sum = @counter.sum) < 0 ? 0 : sum # ignore transient negative values
|
535
|
+
end
|
536
|
+
|
537
|
+
def empty?
|
538
|
+
size == 0
|
539
|
+
end
|
540
|
+
|
541
|
+
# Implementation for clear. Steps through each bin, removing all nodes.
|
542
|
+
def clear
|
543
|
+
return self unless current_table = table
|
544
|
+
current_table_size = current_table.size
|
545
|
+
deleted_count = i = 0
|
546
|
+
while i < current_table_size
|
547
|
+
if !(node = current_table.volatile_get(i))
|
548
|
+
i += 1
|
549
|
+
elsif (node_hash = node.hash) == MOVED
|
550
|
+
current_table = node.key
|
551
|
+
current_table_size = current_table.size
|
552
|
+
elsif Node.locked_hash?(node_hash)
|
553
|
+
decrement_size(deleted_count) # opportunistically update count
|
554
|
+
deleted_count = 0
|
555
|
+
node.try_await_lock(current_table, i)
|
556
|
+
else
|
557
|
+
current_table.try_lock_via_hash(i, node, node_hash) do
|
558
|
+
begin
|
559
|
+
deleted_count += 1 if NULL != node.value # recheck under lock
|
560
|
+
node.value = nil
|
561
|
+
end while node = node.next
|
562
|
+
current_table.volatile_set(i, nil)
|
563
|
+
i += 1
|
564
|
+
end
|
565
|
+
end
|
566
|
+
end
|
567
|
+
decrement_size(deleted_count)
|
568
|
+
self
|
569
|
+
end
|
570
|
+
|
571
|
+
private
|
572
|
+
# Internal versions of the insertion methods, each a
|
573
|
+
# little more complicated than the last. All have
|
574
|
+
# the same basic structure:
|
575
|
+
# 1. If table uninitialized, create
|
576
|
+
# 2. If bin empty, try to CAS new node
|
577
|
+
# 3. If bin stale, use new table
|
578
|
+
# 4. Lock and validate; if valid, scan and add or update
|
579
|
+
#
|
580
|
+
# The others interweave other checks and/or alternative actions:
|
581
|
+
# * Plain +get_and_set+ checks for and performs resize after insertion.
|
582
|
+
# * compute_if_absent prescans for mapping without lock (and fails to add
|
583
|
+
# if present), which also makes pre-emptive resize checks worthwhile.
|
584
|
+
#
|
585
|
+
# Someday when details settle down a bit more, it might be worth
|
586
|
+
# some factoring to reduce sprawl.
|
587
|
+
def internal_replace(key, expected_old_value = NULL, &block)
|
588
|
+
hash = key_hash(key)
|
589
|
+
current_table = table
|
590
|
+
while current_table
|
591
|
+
if !(node = current_table.volatile_get(i = current_table.hash_to_index(hash)))
|
592
|
+
break
|
593
|
+
elsif (node_hash = node.hash) == MOVED
|
594
|
+
current_table = node.key
|
595
|
+
elsif (node_hash & HASH_BITS) != hash && !node.next # precheck
|
596
|
+
break # rules out possible existence
|
597
|
+
elsif Node.locked_hash?(node_hash)
|
598
|
+
try_await_lock(current_table, i, node)
|
599
|
+
else
|
600
|
+
succeeded, old_value = attempt_internal_replace(key, expected_old_value, hash, current_table, i, node, node_hash, &block)
|
601
|
+
return old_value if succeeded
|
602
|
+
end
|
603
|
+
end
|
604
|
+
NULL
|
605
|
+
end
|
606
|
+
|
607
|
+
def attempt_internal_replace(key, expected_old_value, hash, current_table, i, node, node_hash)
|
608
|
+
current_table.try_lock_via_hash(i, node, node_hash) do
|
609
|
+
predecessor_node = nil
|
610
|
+
old_value = NULL
|
611
|
+
begin
|
612
|
+
if node.matches?(key, hash) && NULL != (current_value = node.value)
|
613
|
+
if NULL == expected_old_value || expected_old_value == current_value # NULL == expected_old_value means whatever value
|
614
|
+
old_value = current_value
|
615
|
+
if NULL == (node.value = yield(old_value))
|
616
|
+
current_table.delete_node_at(i, node, predecessor_node)
|
617
|
+
decrement_size
|
618
|
+
end
|
619
|
+
end
|
620
|
+
break
|
621
|
+
end
|
622
|
+
|
623
|
+
predecessor_node = node
|
624
|
+
end while node = node.next
|
625
|
+
|
626
|
+
return true, old_value
|
627
|
+
end
|
628
|
+
end
|
629
|
+
|
630
|
+
def find_value_in_node_list(node, key, hash, pure_hash)
|
631
|
+
do_check_for_resize = false
|
632
|
+
while true
|
633
|
+
if pure_hash == hash && node.key?(key) && NULL != (value = node.value)
|
634
|
+
return value
|
635
|
+
elsif node = node.next
|
636
|
+
do_check_for_resize = true # at least 2 nodes -> check for resize
|
637
|
+
pure_hash = node.pure_hash
|
638
|
+
else
|
639
|
+
return NULL
|
640
|
+
end
|
641
|
+
end
|
642
|
+
ensure
|
643
|
+
check_for_resize if do_check_for_resize
|
644
|
+
end
|
645
|
+
|
646
|
+
def internal_compute(key, &block)
|
647
|
+
hash = key_hash(key)
|
648
|
+
current_table = table || initialize_table
|
649
|
+
while true
|
650
|
+
if !(node = current_table.volatile_get(i = current_table.hash_to_index(hash)))
|
651
|
+
succeeded, new_value = current_table.try_to_cas_in_computed(i, hash, key, &block)
|
652
|
+
if succeeded
|
653
|
+
if NULL == new_value
|
654
|
+
break nil
|
655
|
+
else
|
656
|
+
increment_size
|
657
|
+
break new_value
|
658
|
+
end
|
659
|
+
end
|
660
|
+
elsif (node_hash = node.hash) == MOVED
|
661
|
+
current_table = node.key
|
662
|
+
elsif Node.locked_hash?(node_hash)
|
663
|
+
try_await_lock(current_table, i, node)
|
664
|
+
else
|
665
|
+
succeeded, new_value = attempt_compute(key, hash, current_table, i, node, node_hash, &block)
|
666
|
+
break new_value if succeeded
|
667
|
+
end
|
668
|
+
end
|
669
|
+
end
|
670
|
+
|
671
|
+
def attempt_internal_compute_if_absent(key, hash, current_table, i, node, node_hash)
|
672
|
+
added = false
|
673
|
+
current_table.try_lock_via_hash(i, node, node_hash) do
|
674
|
+
while true
|
675
|
+
if node.matches?(key, hash) && NULL != (value = node.value)
|
676
|
+
return true, value
|
677
|
+
end
|
678
|
+
last = node
|
679
|
+
unless node = node.next
|
680
|
+
last.next = Node.new(hash, key, value = yield)
|
681
|
+
added = true
|
682
|
+
increment_size
|
683
|
+
return true, value
|
684
|
+
end
|
685
|
+
end
|
686
|
+
end
|
687
|
+
ensure
|
688
|
+
check_for_resize if added
|
689
|
+
end
|
690
|
+
|
691
|
+
def attempt_compute(key, hash, current_table, i, node, node_hash)
|
692
|
+
added = false
|
693
|
+
current_table.try_lock_via_hash(i, node, node_hash) do
|
694
|
+
predecessor_node = nil
|
695
|
+
while true
|
696
|
+
if node.matches?(key, hash) && NULL != (value = node.value)
|
697
|
+
if NULL == (node.value = value = yield(value))
|
698
|
+
current_table.delete_node_at(i, node, predecessor_node)
|
699
|
+
decrement_size
|
700
|
+
value = nil
|
701
|
+
end
|
702
|
+
return true, value
|
703
|
+
end
|
704
|
+
predecessor_node = node
|
705
|
+
unless node = node.next
|
706
|
+
if NULL == (value = yield(NULL))
|
707
|
+
value = nil
|
708
|
+
else
|
709
|
+
predecessor_node.next = Node.new(hash, key, value)
|
710
|
+
added = true
|
711
|
+
increment_size
|
712
|
+
end
|
713
|
+
return true, value
|
714
|
+
end
|
715
|
+
end
|
716
|
+
end
|
717
|
+
ensure
|
718
|
+
check_for_resize if added
|
719
|
+
end
|
720
|
+
|
721
|
+
def attempt_get_and_set(key, value, hash, current_table, i, node, node_hash)
|
722
|
+
node_nesting = nil
|
723
|
+
current_table.try_lock_via_hash(i, node, node_hash) do
|
724
|
+
node_nesting = 1
|
725
|
+
old_value = nil
|
726
|
+
found_old_value = false
|
727
|
+
while node
|
728
|
+
if node.matches?(key, hash) && NULL != (old_value = node.value)
|
729
|
+
found_old_value = true
|
730
|
+
node.value = value
|
731
|
+
break
|
732
|
+
end
|
733
|
+
last = node
|
734
|
+
unless node = node.next
|
735
|
+
last.next = Node.new(hash, key, value)
|
736
|
+
break
|
737
|
+
end
|
738
|
+
node_nesting += 1
|
739
|
+
end
|
740
|
+
|
741
|
+
return true, old_value if found_old_value
|
742
|
+
increment_size
|
743
|
+
true
|
744
|
+
end
|
745
|
+
ensure
|
746
|
+
check_for_resize if node_nesting && (node_nesting > 1 || current_table.size <= 64)
|
747
|
+
end
|
748
|
+
|
749
|
+
def initialize_copy(other)
|
750
|
+
super
|
751
|
+
@counter = Util::Adder.new
|
752
|
+
self.table = nil
|
753
|
+
self.size_control = (other_table = other.table) ? other_table.size : DEFAULT_CAPACITY
|
754
|
+
self
|
755
|
+
end
|
756
|
+
|
757
|
+
def try_await_lock(current_table, i, node)
|
758
|
+
check_for_resize # try resizing if can't get lock
|
759
|
+
node.try_await_lock(current_table, i)
|
760
|
+
end
|
761
|
+
|
762
|
+
def key_hash(key)
|
763
|
+
key.hash & HASH_BITS
|
764
|
+
end
|
765
|
+
|
766
|
+
# Returns a power of two table size for the given desired capacity.
|
767
|
+
def table_size_for(entry_count)
|
768
|
+
size = 2
|
769
|
+
size <<= 1 while size < entry_count
|
770
|
+
size
|
771
|
+
end
|
772
|
+
|
773
|
+
# Initializes table, using the size recorded in +size_control+.
|
774
|
+
def initialize_table
|
775
|
+
until current_table ||= table
|
776
|
+
if (size_ctrl = size_control) == NOW_RESIZING
|
777
|
+
Thread.pass # lost initialization race; just spin
|
778
|
+
else
|
779
|
+
try_in_resize_lock(current_table, size_ctrl) do
|
780
|
+
initial_size = size_ctrl > 0 ? size_ctrl : DEFAULT_CAPACITY
|
781
|
+
current_table = self.table = Table.new(initial_size)
|
782
|
+
initial_size - (initial_size >> 2) # 75% load factor
|
783
|
+
end
|
784
|
+
end
|
785
|
+
end
|
786
|
+
current_table
|
787
|
+
end
|
788
|
+
|
789
|
+
# If table is too small and not already resizing, creates next
|
790
|
+
# table and transfers bins. Rechecks occupancy after a transfer
|
791
|
+
# to see if another resize is already needed because resizings
|
792
|
+
# are lagging additions.
|
793
|
+
def check_for_resize
|
794
|
+
while (current_table = table) && MAX_CAPACITY > (table_size = current_table.size) && NOW_RESIZING != (size_ctrl = size_control) && size_ctrl < @counter.sum
|
795
|
+
try_in_resize_lock(current_table, size_ctrl) do
|
796
|
+
self.table = rebuild(current_table)
|
797
|
+
(table_size << 1) - (table_size >> 1) # 75% load factor
|
798
|
+
end
|
799
|
+
end
|
800
|
+
end
|
801
|
+
|
802
|
+
def try_in_resize_lock(current_table, size_ctrl)
|
803
|
+
if cas_size_control(size_ctrl, NOW_RESIZING)
|
804
|
+
begin
|
805
|
+
if current_table == table # recheck under lock
|
806
|
+
size_ctrl = yield # get new size_control
|
807
|
+
end
|
808
|
+
ensure
|
809
|
+
self.size_control = size_ctrl
|
810
|
+
end
|
811
|
+
end
|
812
|
+
end
|
813
|
+
|
814
|
+
# Moves and/or copies the nodes in each bin to new table. See above for explanation.
|
815
|
+
def rebuild(table)
|
816
|
+
old_table_size = table.size
|
817
|
+
new_table = table.next_in_size_table
|
818
|
+
# puts "#{old_table_size} -> #{new_table.size}"
|
819
|
+
forwarder = Node.new(MOVED, new_table, NULL)
|
820
|
+
rev_forwarder = nil
|
821
|
+
locked_indexes = nil # holds bins to revisit; nil until needed
|
822
|
+
locked_arr_idx = 0
|
823
|
+
bin = old_table_size - 1
|
824
|
+
i = bin
|
825
|
+
while true
|
826
|
+
if !(node = table.volatile_get(i))
|
827
|
+
# no lock needed (or available) if bin >= 0, because we're not popping values from locked_indexes until we've run through the whole table
|
828
|
+
redo unless (bin >= 0 ? table.cas(i, nil, forwarder) : lock_and_clean_up_reverse_forwarders(table, old_table_size, new_table, i, forwarder))
|
829
|
+
elsif Node.locked_hash?(node_hash = node.hash)
|
830
|
+
locked_indexes ||= Array.new
|
831
|
+
if bin < 0 && locked_arr_idx > 0
|
832
|
+
locked_arr_idx -= 1
|
833
|
+
i, locked_indexes[locked_arr_idx] = locked_indexes[locked_arr_idx], i # swap with another bin
|
834
|
+
redo
|
835
|
+
end
|
836
|
+
if bin < 0 || locked_indexes.size >= TRANSFER_BUFFER_SIZE
|
837
|
+
node.try_await_lock(table, i) # no other options -- block
|
838
|
+
redo
|
839
|
+
end
|
840
|
+
rev_forwarder ||= Node.new(MOVED, table, NULL)
|
841
|
+
redo unless table.volatile_get(i) == node && node.locked? # recheck before adding to list
|
842
|
+
locked_indexes << i
|
843
|
+
new_table.volatile_set(i, rev_forwarder)
|
844
|
+
new_table.volatile_set(i + old_table_size, rev_forwarder)
|
845
|
+
else
|
846
|
+
redo unless split_old_bin(table, new_table, i, node, node_hash, forwarder)
|
847
|
+
end
|
848
|
+
|
849
|
+
if bin > 0
|
850
|
+
i = (bin -= 1)
|
851
|
+
elsif locked_indexes && !locked_indexes.empty?
|
852
|
+
bin = -1
|
853
|
+
i = locked_indexes.pop
|
854
|
+
locked_arr_idx = locked_indexes.size - 1
|
855
|
+
else
|
856
|
+
return new_table
|
857
|
+
end
|
858
|
+
end
|
859
|
+
end
|
860
|
+
|
861
|
+
def lock_and_clean_up_reverse_forwarders(old_table, old_table_size, new_table, i, forwarder)
|
862
|
+
# transiently use a locked forwarding node
|
863
|
+
locked_forwarder = Node.new(moved_locked_hash = MOVED | LOCKED, new_table, NULL)
|
864
|
+
if old_table.cas(i, nil, locked_forwarder)
|
865
|
+
new_table.volatile_set(i, nil) # kill the potential reverse forwarders
|
866
|
+
new_table.volatile_set(i + old_table_size, nil) # kill the potential reverse forwarders
|
867
|
+
old_table.volatile_set(i, forwarder)
|
868
|
+
locked_forwarder.unlock_via_hash(moved_locked_hash, MOVED)
|
869
|
+
true
|
870
|
+
end
|
871
|
+
end
|
872
|
+
|
873
|
+
# Splits a normal bin with list headed by e into lo and hi parts; installs in given table.
|
874
|
+
def split_old_bin(table, new_table, i, node, node_hash, forwarder)
|
875
|
+
table.try_lock_via_hash(i, node, node_hash) do
|
876
|
+
split_bin(new_table, i, node, node_hash)
|
877
|
+
table.volatile_set(i, forwarder)
|
878
|
+
end
|
879
|
+
end
|
880
|
+
|
881
|
+
def split_bin(new_table, i, node, node_hash)
|
882
|
+
bit = new_table.size >> 1 # bit to split on
|
883
|
+
run_bit = node_hash & bit
|
884
|
+
last_run = nil
|
885
|
+
low = nil
|
886
|
+
high = nil
|
887
|
+
current_node = node
|
888
|
+
# this optimises for the lowest amount of volatile writes and objects created
|
889
|
+
while current_node = current_node.next
|
890
|
+
unless (b = current_node.hash & bit) == run_bit
|
891
|
+
run_bit = b
|
892
|
+
last_run = current_node
|
893
|
+
end
|
894
|
+
end
|
895
|
+
if run_bit == 0
|
896
|
+
low = last_run
|
897
|
+
else
|
898
|
+
high = last_run
|
899
|
+
end
|
900
|
+
current_node = node
|
901
|
+
until current_node == last_run
|
902
|
+
pure_hash = current_node.pure_hash
|
903
|
+
if (pure_hash & bit) == 0
|
904
|
+
low = Node.new(pure_hash, current_node.key, current_node.value, low)
|
905
|
+
else
|
906
|
+
high = Node.new(pure_hash, current_node.key, current_node.value, high)
|
907
|
+
end
|
908
|
+
current_node = current_node.next
|
909
|
+
end
|
910
|
+
new_table.volatile_set(i, low)
|
911
|
+
new_table.volatile_set(i + bit, high)
|
912
|
+
end
|
913
|
+
|
914
|
+
def increment_size
|
915
|
+
@counter.increment
|
916
|
+
end
|
917
|
+
|
918
|
+
def decrement_size(by = 1)
|
919
|
+
@counter.add(-by)
|
920
|
+
end
|
921
|
+
end
|
922
|
+
end
|