Skip to content

Commit 3a0bbef

Browse files
committed
Initial import of the thread_safe gem.
1 parent 1fbf9d5 commit 3a0bbef

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+13143
-27
lines changed

README.md

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -69,24 +69,32 @@ This library contains a variety of concurrency abstractions at high and low leve
6969
* [ScheduledTask](http://ruby-concurrency.github.io/concurrent-ruby/Concurrent/ScheduledTask.html): Like a Future scheduled for a specific future time.
7070
* [TimerTask](http://ruby-concurrency.github.io/concurrent-ruby/Concurrent/TimerTask.html): A Thread that periodically wakes up to perform work at regular intervals.
7171

72+
#### Thread-safe Collection Classes
73+
74+
These classes were originally part of the (deprecated) `thread_safe` gem.
75+
76+
* [Array](http://ruby-concurrency.github.io/concurrent-ruby/Concurrent/Array.html) A thread-safe subclass of Ruby's standard [Array](http://ruby-doc.org/core-2.2.0/Array.html).
77+
* [Hash](http://ruby-concurrency.github.io/concurrent-ruby/Concurrent/Hash.html) A thread-safe subclass of Ruby's standard [Hash](http://ruby-doc.org/core-2.2.0/Hash.html).
78+
* [Cache](http://ruby-concurrency.github.io/concurrent-ruby/Concurrent/Cache.html) A hash-like object that should have much better performance characteristics, especially under high concurrency, than `Concurrent::Hash`.
79+
7280
#### Thread-safe Value Objects
7381

74-
* `Maybe` A thread-safe, immutable object representing an optional value, based on
82+
* [Maybe](http://ruby-concurrency.github.io/concurrent-ruby/Concurrent/Maybe.html) A thread-safe, immutable object representing an optional value, based on
7583
[Haskell Data.Maybe](https://hackage.haskell.org/package/base-4.2.0.1/docs/Data-Maybe.html).
76-
* `Delay` Lazy evaluation of a block yielding an immutable result. Based on Clojure's
84+
* [Delay](http://ruby-concurrency.github.io/concurrent-ruby/Concurrent/Delay.html) Lazy evaluation of a block yielding an immutable result. Based on Clojure's
7785
[delay](http://ruby-concurrency.github.io/concurrent-ruby/Concurrent/Delay.html).
7886

7987
#### Thread-safe Structures
8088

8189
Derived from Ruby's [Struct](http://ruby-doc.org/core-2.2.0/Struct.html):
8290

83-
* `ImmutableStruct` Immutable struct where values are set at construction and cannot be changed later.
84-
* `MutableStruct` Synchronized, mutable struct where values can be safely changed at any time.
85-
* `SettableStruct` Synchronized, write-once struct where values can be set at most once, either at construction or any time thereafter.
91+
* [ImmutableStruct](http://ruby-concurrency.github.io/concurrent-ruby/Concurrent/ImmutableStruct.html) Immutable struct where values are set at construction and cannot be changed later.
92+
* [MutableStruct](http://ruby-concurrency.github.io/concurrent-ruby/Concurrent/MutableStruct.html) Synchronized, mutable struct where values can be safely changed at any time.
93+
* [SettableStruct](http://ruby-concurrency.github.io/concurrent-ruby/Concurrent/SettableStruct.html) Synchronized, write-once struct where values can be set at most once, either at construction or any time thereafter.
8694

8795
#### Java-inspired ThreadPools and Other Executors
8896

89-
* See [ThreadPool](http://ruby-concurrency.github.io/concurrent-ruby/file.thread_pools.html) overview, which also contains a list of other Executors available.
97+
* See the [thread pool](http://ruby-concurrency.github.io/concurrent-ruby/file.thread_pools.html) overview, which also contains a list of other Executors available.
9098

9199
#### Thread Synchronization Classes and Algorithms
92100

@@ -272,11 +280,17 @@ bundle exec rake compile # Compile all the extensions
272280
## Maintainers
273281

274282
* [Jerry D'Antonio](https://github.com/jdantonio) (creator)
283+
* [Petr Chalupa](https://github.com/pitr-ch)
275284
* [Michele Della Torre](https://github.com/mighe)
276285
* [Chris Seaton](https://github.com/chrisseaton)
277-
* [Lucas Allan](https://github.com/lucasallan)
278-
* [Petr Chalupa](https://github.com/pitr-ch)
279286
* [Paweł Obrok](https://github.com/obrok)
287+
* [Lucas Allan](https://github.com/lucasallan)
288+
289+
### Special Thanks
290+
291+
* [Brian Durand](https://github.com/bdurand) for the `ref` gem
292+
* [Charles Oliver Nutter](https://github.com/headius) for the `atomic` and `thread_safe` gems
293+
* [thedarkone](https://github.com/thedarkone) for the `thread_safe` gem
280294

281295
## Contributing
282296

examples/bench_cache.rb

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#!/usr/bin/env ruby
2+
3+
require "benchmark"
4+
require "concurrent"
5+
6+
hash = {}
7+
cache = Concurrent::Cache.new
8+
9+
ENTRIES = 10_000
10+
11+
ENTRIES.times do |i|
12+
hash[i] = i
13+
cache[i] = i
14+
end
15+
16+
TESTS = 40_000_000
17+
Benchmark.bmbm do |results|
18+
key = rand(10_000)
19+
20+
results.report('Hash#[]') do
21+
TESTS.times { hash[key] }
22+
end
23+
24+
results.report('Cache#[]') do
25+
TESTS.times { cache[key] }
26+
end
27+
28+
results.report('Hash#each_pair') do
29+
(TESTS / ENTRIES).times { hash.each_pair {|k,v| v} }
30+
end
31+
32+
results.report('Cache#each_pair') do
33+
(TESTS / ENTRIES).times { cache.each_pair {|k,v| v} }
34+
end
35+
end

examples/bench_map.rb

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#!/usr/bin/env ruby -wU
2+
3+
require 'benchmark'
4+
require 'concurrent'
5+
6+
hash = {}
7+
cache = Concurrent::Map.new
8+
9+
ENTRIES = 10_000
10+
11+
ENTRIES.times do |i|
12+
hash[i] = i
13+
cache[i] = i
14+
end
15+
16+
TESTS = 40_000_000
17+
Benchmark.bmbm do |results|
18+
key = rand(10_000)
19+
20+
results.report('Hash#[]') do
21+
TESTS.times { hash[key] }
22+
end
23+
24+
results.report('Map#[]') do
25+
TESTS.times { cache[key] }
26+
end
27+
28+
results.report('Hash#each_pair') do
29+
(TESTS / ENTRIES).times { hash.each_pair {|k,v| v} }
30+
end
31+
32+
results.report('Map#each_pair') do
33+
(TESTS / ENTRIES).times { cache.each_pair {|k,v| v} }
34+
end
35+
end

ext/ConcurrentRubyExtService.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import org.jruby.Ruby;
44
import org.jruby.runtime.load.BasicLibraryService;
5+
import com.concurrent_ruby.ext.JRubyCacheBackendLibrary;
56

67
public class ConcurrentRubyExtService implements BasicLibraryService {
78
public boolean basicLoad(final Ruby runtime) throws IOException {
@@ -10,6 +11,7 @@ public boolean basicLoad(final Ruby runtime) throws IOException {
1011
new com.concurrent_ruby.ext.JavaAtomicFixnumLibrary().load(runtime, false);
1112
new com.concurrent_ruby.ext.JavaSemaphoreLibrary().load(runtime, false);
1213
new com.concurrent_ruby.ext.SynchronizationLibrary().load(runtime, false);
14+
new com.concurrent_ruby.ext.JRubyCacheBackendLibrary().load(runtime, false);
1315
return true;
1416
}
1517
}
Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
package com.concurrent_ruby.ext;
2+
3+
import org.jruby.*;
4+
import org.jruby.anno.JRubyClass;
5+
import org.jruby.anno.JRubyMethod;
6+
import com.concurrent_ruby.ext.jsr166e.ConcurrentHashMap;
7+
import com.concurrent_ruby.ext.jsr166e.ConcurrentHashMapV8;
8+
import com.concurrent_ruby.ext.jsr166e.nounsafe.*;
9+
import org.jruby.runtime.Block;
10+
import org.jruby.runtime.ObjectAllocator;
11+
import org.jruby.runtime.ThreadContext;
12+
import org.jruby.runtime.builtin.IRubyObject;
13+
import org.jruby.runtime.load.Library;
14+
15+
import java.io.IOException;
16+
import java.util.Map;
17+
18+
import static org.jruby.runtime.Visibility.PRIVATE;
19+
20+
/**
21+
* Native Java implementation to avoid the JI overhead.
22+
*
23+
* @author thedarkone
24+
*/
25+
public class JRubyCacheBackendLibrary implements Library {
26+
public void load(Ruby runtime, boolean wrap) throws IOException {
27+
28+
RubyModule concurrentMod = runtime.defineModule("Concurrent");
29+
RubyModule thread_safeMod = concurrentMod.defineModuleUnder("ThreadSafe");
30+
RubyClass jrubyRefClass = thread_safeMod.defineClassUnder("JRubyCacheBackend", runtime.getObject(), BACKEND_ALLOCATOR);
31+
jrubyRefClass.setAllocator(BACKEND_ALLOCATOR);
32+
jrubyRefClass.defineAnnotatedMethods(JRubyCacheBackend.class);
33+
}
34+
35+
private static final ObjectAllocator BACKEND_ALLOCATOR = new ObjectAllocator() {
36+
public IRubyObject allocate(Ruby runtime, RubyClass klazz) {
37+
return new JRubyCacheBackend(runtime, klazz);
38+
}
39+
};
40+
41+
@JRubyClass(name="JRubyCacheBackend", parent="Object")
42+
public static class JRubyCacheBackend extends RubyObject {
43+
// Defaults used by the CHM
44+
static final int DEFAULT_INITIAL_CAPACITY = 16;
45+
static final float DEFAULT_LOAD_FACTOR = 0.75f;
46+
47+
public static final boolean CAN_USE_UNSAFE_CHM = canUseUnsafeCHM();
48+
49+
private ConcurrentHashMap<IRubyObject, IRubyObject> map;
50+
51+
private static ConcurrentHashMap<IRubyObject, IRubyObject> newCHM(int initialCapacity, float loadFactor) {
52+
if (CAN_USE_UNSAFE_CHM) {
53+
return new ConcurrentHashMapV8<IRubyObject, IRubyObject>(initialCapacity, loadFactor);
54+
} else {
55+
return new com.concurrent_ruby.ext.jsr166e.nounsafe.ConcurrentHashMapV8<IRubyObject, IRubyObject>(initialCapacity, loadFactor);
56+
}
57+
}
58+
59+
private static ConcurrentHashMap<IRubyObject, IRubyObject> newCHM() {
60+
return newCHM(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
61+
}
62+
63+
private static boolean canUseUnsafeCHM() {
64+
try {
65+
new com.concurrent_ruby.ext.jsr166e.ConcurrentHashMapV8(); // force class load and initialization
66+
return true;
67+
} catch (Throwable t) { // ensuring we really do catch everything
68+
// Doug's Unsafe setup errors always have this "Could not ini.." message
69+
if (isCausedBySecurityException(t)) {
70+
return false;
71+
}
72+
throw (t instanceof RuntimeException ? (RuntimeException) t : new RuntimeException(t));
73+
}
74+
}
75+
76+
private static boolean isCausedBySecurityException(Throwable t) {
77+
while (t != null) {
78+
if ((t.getMessage() != null && t.getMessage().contains("Could not initialize intrinsics")) || t instanceof SecurityException) {
79+
return true;
80+
}
81+
t = t.getCause();
82+
}
83+
return false;
84+
}
85+
86+
public JRubyCacheBackend(Ruby runtime, RubyClass klass) {
87+
super(runtime, klass);
88+
}
89+
90+
@JRubyMethod
91+
public IRubyObject initialize(ThreadContext context) {
92+
map = newCHM();
93+
return context.getRuntime().getNil();
94+
}
95+
96+
@JRubyMethod
97+
public IRubyObject initialize(ThreadContext context, IRubyObject options) {
98+
map = toCHM(context, options);
99+
return context.getRuntime().getNil();
100+
}
101+
102+
private ConcurrentHashMap<IRubyObject, IRubyObject> toCHM(ThreadContext context, IRubyObject options) {
103+
Ruby runtime = context.getRuntime();
104+
if (!options.isNil() && options.respondsTo("[]")) {
105+
IRubyObject rInitialCapacity = options.callMethod(context, "[]", runtime.newSymbol("initial_capacity"));
106+
IRubyObject rLoadFactor = options.callMethod(context, "[]", runtime.newSymbol("load_factor"));
107+
int initialCapacity = !rInitialCapacity.isNil() ? RubyNumeric.num2int(rInitialCapacity.convertToInteger()) : DEFAULT_INITIAL_CAPACITY;
108+
float loadFactor = !rLoadFactor.isNil() ? (float)RubyNumeric.num2dbl(rLoadFactor.convertToFloat()) : DEFAULT_LOAD_FACTOR;
109+
return newCHM(initialCapacity, loadFactor);
110+
} else {
111+
return newCHM();
112+
}
113+
}
114+
115+
@JRubyMethod(name = "[]", required = 1)
116+
public IRubyObject op_aref(ThreadContext context, IRubyObject key) {
117+
IRubyObject value;
118+
return ((value = map.get(key)) == null) ? context.getRuntime().getNil() : value;
119+
}
120+
121+
@JRubyMethod(name = {"[]="}, required = 2)
122+
public IRubyObject op_aset(IRubyObject key, IRubyObject value) {
123+
map.put(key, value);
124+
return value;
125+
}
126+
127+
@JRubyMethod
128+
public IRubyObject put_if_absent(IRubyObject key, IRubyObject value) {
129+
IRubyObject result = map.putIfAbsent(key, value);
130+
return result == null ? getRuntime().getNil() : result;
131+
}
132+
133+
@JRubyMethod
134+
public IRubyObject compute_if_absent(final ThreadContext context, final IRubyObject key, final Block block) {
135+
return map.computeIfAbsent(key, new ConcurrentHashMap.Fun<IRubyObject, IRubyObject>() {
136+
@Override
137+
public IRubyObject apply(IRubyObject key) {
138+
return block.yieldSpecific(context);
139+
}
140+
});
141+
}
142+
143+
@JRubyMethod
144+
public IRubyObject compute_if_present(final ThreadContext context, final IRubyObject key, final Block block) {
145+
IRubyObject result = map.computeIfPresent(key, new ConcurrentHashMap.BiFun<IRubyObject, IRubyObject, IRubyObject>() {
146+
@Override
147+
public IRubyObject apply(IRubyObject key, IRubyObject oldValue) {
148+
IRubyObject result = block.yieldSpecific(context, oldValue == null ? context.getRuntime().getNil() : oldValue);
149+
return result.isNil() ? null : result;
150+
}
151+
});
152+
return result == null ? context.getRuntime().getNil() : result;
153+
}
154+
155+
@JRubyMethod
156+
public IRubyObject compute(final ThreadContext context, final IRubyObject key, final Block block) {
157+
IRubyObject result = map.compute(key, new ConcurrentHashMap.BiFun<IRubyObject, IRubyObject, IRubyObject>() {
158+
@Override
159+
public IRubyObject apply(IRubyObject key, IRubyObject oldValue) {
160+
IRubyObject result = block.yieldSpecific(context, oldValue == null ? context.getRuntime().getNil() : oldValue);
161+
return result.isNil() ? null : result;
162+
}
163+
});
164+
return result == null ? context.getRuntime().getNil() : result;
165+
}
166+
167+
@JRubyMethod
168+
public IRubyObject merge_pair(final ThreadContext context, final IRubyObject key, final IRubyObject value, final Block block) {
169+
IRubyObject result = map.merge(key, value, new ConcurrentHashMap.BiFun<IRubyObject, IRubyObject, IRubyObject>() {
170+
@Override
171+
public IRubyObject apply(IRubyObject oldValue, IRubyObject newValue) {
172+
IRubyObject result = block.yieldSpecific(context, oldValue == null ? context.getRuntime().getNil() : oldValue);
173+
return result.isNil() ? null : result;
174+
}
175+
});
176+
return result == null ? context.getRuntime().getNil() : result;
177+
}
178+
179+
@JRubyMethod
180+
public RubyBoolean replace_pair(IRubyObject key, IRubyObject oldValue, IRubyObject newValue) {
181+
return getRuntime().newBoolean(map.replace(key, oldValue, newValue));
182+
}
183+
184+
@JRubyMethod(name = "key?", required = 1)
185+
public RubyBoolean has_key_p(IRubyObject key) {
186+
return map.containsKey(key) ? getRuntime().getTrue() : getRuntime().getFalse();
187+
}
188+
189+
@JRubyMethod
190+
public IRubyObject key(IRubyObject value) {
191+
final IRubyObject key = map.findKey(value);
192+
return key == null ? getRuntime().getNil() : key;
193+
}
194+
195+
@JRubyMethod
196+
public IRubyObject replace_if_exists(IRubyObject key, IRubyObject value) {
197+
IRubyObject result = map.replace(key, value);
198+
return result == null ? getRuntime().getNil() : result;
199+
}
200+
201+
@JRubyMethod
202+
public IRubyObject get_and_set(IRubyObject key, IRubyObject value) {
203+
IRubyObject result = map.put(key, value);
204+
return result == null ? getRuntime().getNil() : result;
205+
}
206+
207+
@JRubyMethod
208+
public IRubyObject delete(IRubyObject key) {
209+
IRubyObject result = map.remove(key);
210+
return result == null ? getRuntime().getNil() : result;
211+
}
212+
213+
@JRubyMethod
214+
public RubyBoolean delete_pair(IRubyObject key, IRubyObject value) {
215+
return getRuntime().newBoolean(map.remove(key, value));
216+
}
217+
218+
@JRubyMethod
219+
public IRubyObject clear() {
220+
map.clear();
221+
return this;
222+
}
223+
224+
@JRubyMethod
225+
public IRubyObject each_pair(ThreadContext context, Block block) {
226+
for (Map.Entry<IRubyObject,IRubyObject> entry : map.entrySet()) {
227+
block.yieldSpecific(context, entry.getKey(), entry.getValue());
228+
}
229+
return this;
230+
}
231+
232+
@JRubyMethod
233+
public RubyFixnum size(ThreadContext context) {
234+
return context.getRuntime().newFixnum(map.size());
235+
}
236+
237+
@JRubyMethod
238+
public IRubyObject get_or_default(IRubyObject key, IRubyObject defaultValue) {
239+
return map.getValueOrDefault(key, defaultValue);
240+
}
241+
242+
@JRubyMethod(visibility = PRIVATE)
243+
public JRubyCacheBackend initialize_copy(ThreadContext context, IRubyObject other) {
244+
map = newCHM();
245+
return this;
246+
}
247+
}
248+
}

0 commit comments

Comments
 (0)