concurrent-ruby 1.2.2 → 1.3.1.pre

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