thread_safe 0.0.3 → 0.1.0

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.
@@ -0,0 +1,137 @@
1
+ require 'thread'
2
+
3
+ module ThreadSafe
4
+ autoload :JRubyCacheBackend, 'thread_safe/jruby_cache_backend'
5
+ autoload :MriCacheBackend, 'thread_safe/mri_cache_backend'
6
+ autoload :NonConcurrentCacheBackend, 'thread_safe/non_concurrent_cache_backend'
7
+ autoload :AtomicReferenceCacheBackend, 'thread_safe/atomic_reference_cache_backend'
8
+ autoload :SynchronizedCacheBackend, 'thread_safe/synchronized_cache_backend'
9
+
10
+ ConcurrentCacheBackend =
11
+ case defined?(RUBY_ENGINE) && RUBY_ENGINE
12
+ when 'jruby'; JRubyCacheBackend
13
+ when 'ruby'; MriCacheBackend
14
+ when 'rbx'; AtomicReferenceCacheBackend
15
+ else
16
+ warn 'ThreadSafe: unsupported Ruby engine, using a fully synchronized ThreadSafe::Cache implementation' if $VERBOSE
17
+ SynchronizedCacheBackend
18
+ end
19
+
20
+ class Cache < ConcurrentCacheBackend
21
+ KEY_ERROR = defined?(KeyError) ? KeyError : IndexError # there is no KeyError in 1.8 mode
22
+
23
+ def initialize(options = nil, &block)
24
+ if options.kind_of?(::Hash)
25
+ validate_options_hash!(options)
26
+ else
27
+ options = nil
28
+ end
29
+
30
+ super(options)
31
+ @default_proc = block
32
+ end
33
+
34
+ def [](key)
35
+ if value = super
36
+ value
37
+ elsif @default_proc && !key?(key)
38
+ @default_proc.call(self, key)
39
+ else
40
+ value
41
+ end
42
+ end
43
+
44
+ def fetch(key, default_value = NULL)
45
+ if NULL != (value = get_or_default(key, NULL))
46
+ value
47
+ elsif block_given?
48
+ yield key
49
+ elsif NULL != default_value
50
+ default_value
51
+ else
52
+ raise KEY_ERROR, 'key not found'
53
+ end
54
+ end
55
+
56
+ def put_if_absent(key, value)
57
+ computed = false
58
+ result = compute_if_absent(key) do
59
+ computed = true
60
+ value
61
+ end
62
+ computed ? nil : result
63
+ end unless method_defined?(:put_if_absent)
64
+
65
+ def value?(value)
66
+ each_value do |v|
67
+ return true if value.equal?(v)
68
+ end
69
+ false
70
+ end unless method_defined?(:value?)
71
+
72
+ def keys
73
+ arr = []
74
+ each_pair {|k, v| arr << k}
75
+ arr
76
+ end unless method_defined?(:keys)
77
+
78
+ def values
79
+ arr = []
80
+ each_pair {|k, v| arr << v}
81
+ arr
82
+ end unless method_defined?(:values)
83
+
84
+ def each_key
85
+ each_pair {|k, v| yield k}
86
+ end unless method_defined?(:each_key)
87
+
88
+ def each_value
89
+ each_pair {|k, v| yield v}
90
+ end unless method_defined?(:each_value)
91
+
92
+ def empty?
93
+ each_pair {|k, v| return false}
94
+ true
95
+ end unless method_defined?(:empty?)
96
+
97
+ def size
98
+ count = 0
99
+ each_pair {|k, v| count += 1}
100
+ count
101
+ end unless method_defined?(:size)
102
+
103
+ def marshal_dump
104
+ raise TypeError, "can't dump hash with default proc" if @default_proc
105
+ h = {}
106
+ each_pair {|k, v| h[k] = v}
107
+ h
108
+ end
109
+
110
+ def marshal_load(hash)
111
+ initialize
112
+ populate_from(hash)
113
+ end
114
+
115
+ undef :freeze
116
+
117
+ private
118
+ def initialize_copy(other)
119
+ super
120
+ populate_from(other)
121
+ end
122
+
123
+ def populate_from(hash)
124
+ hash.each_pair {|k, v| self[k] = v}
125
+ self
126
+ end
127
+
128
+ def validate_options_hash!(options)
129
+ if (initial_capacity = options[:initial_capacity]) && (!initial_capacity.kind_of?(Fixnum) || initial_capacity < 0)
130
+ raise ArgumentError, ":initial_capacity must be a positive Fixnum"
131
+ end
132
+ if (load_factor = options[:load_factor]) && (!load_factor.kind_of?(Numeric) || load_factor <= 0 || load_factor > 1)
133
+ raise ArgumentError, ":load_factor must be a number between 0 and 1"
134
+ end
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,62 @@
1
+ module ThreadSafe
2
+ class MriCacheBackend < NonConcurrentCacheBackend
3
+ # We can get away with a single global write lock (instead of a per-instance one) because of the GVL/green threads.
4
+ #
5
+ # The previous implementation used `Thread.critical` on 1.8 MRI to implement the 4 composed atomic operations (`put_if_absent`, `replace_pair`,
6
+ # `replace_if_exists`, `delete_pair`) this however doesn't work for `compute_if_absent` because on 1.8 the Mutex class is itself implemented
7
+ # via `Thread.critical` and a call to `Mutex#lock` does not restore the previous `Thread.critical` value (thus any synchronisation clears the
8
+ # `Thread.critical` flag and we loose control). This poses a problem as the provided block might use synchronisation on its own.
9
+ #
10
+ # NOTE: a neat idea of writing a c-ext to manually perform atomic put_if_absent, while relying on Ruby not releasing a GVL while calling
11
+ # a c-ext will not work because of the potentially Ruby implemented `#hash` and `#eql?` key methods.
12
+ WRITE_LOCK = Mutex.new
13
+
14
+ def []=(key, value)
15
+ WRITE_LOCK.synchronize { super }
16
+ end
17
+
18
+ def compute_if_absent(key)
19
+ if stored_value = _get(key) # fast non-blocking path for the most likely case
20
+ stored_value
21
+ else
22
+ WRITE_LOCK.synchronize { super }
23
+ end
24
+ end
25
+
26
+ def compute_if_present(key)
27
+ WRITE_LOCK.synchronize { super }
28
+ end
29
+
30
+ def compute(key)
31
+ WRITE_LOCK.synchronize { super }
32
+ end
33
+
34
+ def merge_pair(key, value)
35
+ WRITE_LOCK.synchronize { super }
36
+ end
37
+
38
+ def replace_pair(key, old_value, new_value)
39
+ WRITE_LOCK.synchronize { super }
40
+ end
41
+
42
+ def replace_if_exists(key, new_value)
43
+ WRITE_LOCK.synchronize { super }
44
+ end
45
+
46
+ def get_and_set(key, value)
47
+ WRITE_LOCK.synchronize { super }
48
+ end
49
+
50
+ def delete(key)
51
+ WRITE_LOCK.synchronize { super }
52
+ end
53
+
54
+ def delete_pair(key, value)
55
+ WRITE_LOCK.synchronize { super }
56
+ end
57
+
58
+ def clear
59
+ WRITE_LOCK.synchronize { super }
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,133 @@
1
+ module ThreadSafe
2
+ class NonConcurrentCacheBackend
3
+ # WARNING: all public methods of the class must operate on the @backend directly without calling each other. This is important
4
+ # because of the SynchronizedCacheBackend which uses a non-reentrant mutex for perfomance reasons.
5
+ def initialize(options = nil)
6
+ @backend = {}
7
+ end
8
+
9
+ def [](key)
10
+ @backend[key]
11
+ end
12
+
13
+ def []=(key, value)
14
+ @backend[key] = value
15
+ end
16
+
17
+ def compute_if_absent(key)
18
+ if NULL != (stored_value = @backend.fetch(key, NULL))
19
+ stored_value
20
+ else
21
+ @backend[key] = yield
22
+ end
23
+ end
24
+
25
+ def replace_pair(key, old_value, new_value)
26
+ if pair?(key, old_value)
27
+ @backend[key] = new_value
28
+ true
29
+ else
30
+ false
31
+ end
32
+ end
33
+
34
+ def replace_if_exists(key, new_value)
35
+ if NULL != (stored_value = @backend.fetch(key, NULL))
36
+ @backend[key] = new_value
37
+ stored_value
38
+ end
39
+ end
40
+
41
+ def compute_if_present(key)
42
+ if NULL != (stored_value = @backend.fetch(key, NULL))
43
+ store_computed_value(key, yield(stored_value))
44
+ end
45
+ end
46
+
47
+ def compute(key)
48
+ store_computed_value(key, yield(@backend[key]))
49
+ end
50
+
51
+ def merge_pair(key, value)
52
+ if NULL == (stored_value = @backend.fetch(key, NULL))
53
+ @backend[key] = value
54
+ else
55
+ store_computed_value(key, yield(stored_value))
56
+ end
57
+ end
58
+
59
+ def get_and_set(key, value)
60
+ stored_value = @backend[key]
61
+ @backend[key] = value
62
+ stored_value
63
+ end
64
+
65
+ def key?(key)
66
+ @backend.key?(key)
67
+ end
68
+
69
+ def value?(value)
70
+ @backend.value?(value)
71
+ end
72
+
73
+ def delete(key)
74
+ @backend.delete(key)
75
+ end
76
+
77
+ def delete_pair(key, value)
78
+ if pair?(key, value)
79
+ @backend.delete(key)
80
+ true
81
+ else
82
+ false
83
+ end
84
+ end
85
+
86
+ def clear
87
+ @backend.clear
88
+ self
89
+ end
90
+
91
+ def each_pair
92
+ dupped_backend.each_pair do |k, v|
93
+ yield k, v
94
+ end
95
+ self
96
+ end
97
+
98
+ def size
99
+ @backend.size
100
+ end
101
+
102
+ def get_or_default(key, default_value)
103
+ @backend.fetch(key, default_value)
104
+ end
105
+
106
+ alias_method :_get, :[]
107
+ alias_method :_set, :[]=
108
+ private :_get, :_set
109
+ private
110
+ def initialize_copy(other)
111
+ super
112
+ @backend = {}
113
+ self
114
+ end
115
+
116
+ def dupped_backend
117
+ @backend.dup
118
+ end
119
+
120
+ def pair?(key, expected_value)
121
+ NULL != (stored_value = @backend.fetch(key, NULL)) && expected_value.equal?(stored_value)
122
+ end
123
+
124
+ def store_computed_value(key, new_value)
125
+ if new_value.nil?
126
+ @backend.delete(key)
127
+ nil
128
+ else
129
+ @backend[key] = new_value
130
+ end
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,76 @@
1
+ module ThreadSafe
2
+ class SynchronizedCacheBackend < NonConcurrentCacheBackend
3
+ require 'mutex_m'
4
+ include Mutex_m
5
+ # WARNING: Mutex_m is a non-reentrant lock, so the synchronized methods are not allowed to call each other.
6
+
7
+ def [](key)
8
+ synchronize { super }
9
+ end
10
+
11
+ def []=(key, value)
12
+ synchronize { super }
13
+ end
14
+
15
+ def compute_if_absent(key)
16
+ synchronize { super }
17
+ end
18
+
19
+ def compute_if_present(key)
20
+ synchronize { super }
21
+ end
22
+
23
+ def compute(key)
24
+ synchronize { super }
25
+ end
26
+
27
+ def merge_pair(key, value)
28
+ synchronize { super }
29
+ end
30
+
31
+ def replace_pair(key, old_value, new_value)
32
+ synchronize { super }
33
+ end
34
+
35
+ def replace_if_exists(key, new_value)
36
+ synchronize { super }
37
+ end
38
+
39
+ def get_and_set(key, value)
40
+ synchronize { super }
41
+ end
42
+
43
+ def key?(key)
44
+ synchronize { super }
45
+ end
46
+
47
+ def value?(value)
48
+ synchronize { super }
49
+ end
50
+
51
+ def delete(key)
52
+ synchronize { super }
53
+ end
54
+
55
+ def delete_pair(key, value)
56
+ synchronize { super }
57
+ end
58
+
59
+ def clear
60
+ synchronize { super }
61
+ end
62
+
63
+ def size
64
+ synchronize { super }
65
+ end
66
+
67
+ def get_or_default(key, default_value)
68
+ synchronize { super }
69
+ end
70
+
71
+ private
72
+ def dupped_backend
73
+ synchronize { super }
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,16 @@
1
+ module ThreadSafe
2
+ module Util
3
+ FIXNUM_BIT_SIZE = (0.size * 8) - 2
4
+ MAX_INT = (2 ** FIXNUM_BIT_SIZE) - 1
5
+ CPU_COUNT = 16 # is there a way to determine this?
6
+
7
+ autoload :AtomicReference, 'thread_safe/util/atomic_reference'
8
+ autoload :Adder, 'thread_safe/util/adder'
9
+ autoload :CheapLockable, 'thread_safe/util/cheap_lockable'
10
+ autoload :PowerOfTwoTuple, 'thread_safe/util/power_of_two_tuple'
11
+ autoload :Striped64, 'thread_safe/util/striped64'
12
+ autoload :Volatile, 'thread_safe/util/volatile'
13
+ autoload :VolatileTuple, 'thread_safe/util/volatile_tuple'
14
+ autoload :XorShiftRandom, 'thread_safe/util/xor_shift_random'
15
+ end
16
+ end
@@ -0,0 +1,59 @@
1
+ module ThreadSafe
2
+ module Util
3
+ # A Ruby port of the Doug Lea's jsr166e.LondAdder class version 1.8 available in public domain.
4
+ # Original source code available here: http://gee.cs.oswego.edu/cgi-bin/viewcvs.cgi/jsr166/src/jsr166e/LongAdder.java?revision=1.8
5
+ #
6
+ # One or more variables that together maintain an initially zero
7
+ # sum. When updates (method +add+) are contended across threads,
8
+ # the set of variables may grow dynamically to reduce contention.
9
+ # Method +sum+ returns the current total combined across the
10
+ # variables maintaining the sum.
11
+ #
12
+ # This class is usually preferable to single +Atomic+ reference when
13
+ # multiple threads update a common sum that is used for purposes such
14
+ # as collecting statistics, not for fine-grained synchronization
15
+ # control. Under low update contention, the two classes have similar
16
+ # characteristics. But under high contention, expected throughput of
17
+ # this class is significantly higher, at the expense of higher space
18
+ # consumption.
19
+ class Adder < Striped64
20
+ # Adds the given value.
21
+ def add(x)
22
+ if (current_cells = cells) || !cas_base_computed {|current_base| current_base + x}
23
+ was_uncontended = true
24
+ hash = hash_code
25
+ unless current_cells && (cell = current_cells.volatile_get_by_hash(hash)) && (was_uncontended = cell.cas_computed {|current_value| current_value + x})
26
+ retry_update(x, hash, was_uncontended) {|current_value| current_value + x}
27
+ end
28
+ end
29
+ end
30
+
31
+ def increment
32
+ add(1)
33
+ end
34
+
35
+ def decrement
36
+ add(-1)
37
+ end
38
+
39
+ # Returns the current sum. The returned value is _NOT_ an
40
+ # atomic snapshot: Invocation in the absence of concurrent
41
+ # updates returns an accurate result, but concurrent updates that
42
+ # occur while the sum is being calculated might not be
43
+ # incorporated.
44
+ def sum
45
+ x = base
46
+ if current_cells = cells
47
+ current_cells.each do |cell|
48
+ x += cell.value if cell
49
+ end
50
+ end
51
+ x
52
+ end
53
+
54
+ def reset
55
+ internal_reset(0)
56
+ end
57
+ end
58
+ end
59
+ end