module ThreadSafe # A Ruby port of the Doug Lea's jsr166e.ConcurrentHashMapV8 class version 1.59 available in public domain. # Original source code available here: http://gee.cs.oswego.edu/cgi-bin/viewcvs.cgi/jsr166/src/jsr166e/ConcurrentHashMapV8.java?revision=1.59 # # The Ruby port skips out the +TreeBin+ (red-black trees for use in bins # whose size exceeds a threshold). # # A hash table supporting full concurrency of retrievals and # high expected concurrency for updates. However, even though all # operations are thread-safe, retrieval operations do _not_ entail locking, # and there is _not_ any support for locking the entire table # in a way that prevents all access. # # Retrieval operations generally do not block, so may overlap with # update operations. Retrievals reflect the results of the most # recently _completed_ update operations holding upon their # onset. (More formally, an update operation for a given key bears a # _happens-before_ relation with any (non +nil+) retrieval for # that key reporting the updated value.) For aggregate operations # such as +clear()+, concurrent retrievals may reflect insertion or removal # of only some entries. Similarly, the +each_pair+ iterator yields elements # reflecting the state of the hash table at some point at or since # the start of the +each_pair+. Bear in mind that the results of # aggregate status methods including +size()+ and +empty?+} are typically # useful only when a map is not undergoing concurrent updates in other # threads. Otherwise the results of these methods reflect transient # states that may be adequate for monitoring or estimation purposes, but not # for program control. # # The table is dynamically expanded when there are too many # collisions (i.e., keys that have distinct hash codes but fall into # the same slot modulo the table size), with the expected average # effect of maintaining roughly two bins per mapping (corresponding # to a 0.75 load factor threshold for resizing). There may be much # variance around this average as mappings are added and removed, but # overall, this maintains a commonly accepted time/space tradeoff for # hash tables. However, resizing this or any other kind of hash # table may be a relatively slow operation. When possible, it is a # good idea to provide a size estimate as an optional :initial_capacity # initializer argument. An additional optional :load_factor constructor # argument provides a further means of customizing initial table capacity # by specifying the table density to be used in calculating the amount of # space to allocate for the given number of elements. Note that using # many keys with exactly the same +hash+ is a sure way to slow down # performance of any hash table. # # == Design overview # # The primary design goal of this hash table is to maintain # concurrent readability (typically method +[]+, but also # iteration and related methods) while minimizing update # contention. Secondary goals are to keep space consumption about # the same or better than plain +Hash+, and to support high # initial insertion rates on an empty table by many threads. # # Each key-value mapping is held in a +Node+. The validation-based # approach explained below leads to a lot of code sprawl because # retry-control precludes factoring into smaller methods. # # The table is lazily initialized to a power-of-two size upon the # first insertion. Each bin in the table normally contains a # list of +Node+s (most often, the list has only zero or one +Node+). # Table accesses require volatile/atomic reads, writes, and # CASes. The lists of nodes within bins are always accurately traversable # under volatile reads, so long as lookups check hash code # and non-nullness of value before checking key equality. # # We use the top two bits of +Node+ hash fields for control # purposes -- they are available anyway because of addressing # constraints. As explained further below, these top bits are # used as follows: # 00 - Normal # 01 - Locked # 11 - Locked and may have a thread waiting for lock # 10 - +Node+ is a forwarding node # # The lower 28 bits of each +Node+'s hash field contain a # the key's hash code, except for forwarding nodes, for which # the lower bits are zero (and so always have hash field == +MOVED+). # # Insertion (via +[]=+ or its variants) of the first node in an # empty bin is performed by just CASing it to the bin. This is # by far the most common case for put operations under most # key/hash distributions. Other update operations (insert, # delete, and replace) require locks. We do not want to waste # the space required to associate a distinct lock object with # each bin, so instead use the first node of a bin list itself as # a lock. Blocking support for these locks relies +Util::CheapLockable. # However, we also need a +try_lock+ construction, so we overlay # these by using bits of the +Node+ hash field for lock control (see above), # and so normally use builtin monitors only for blocking and signalling using # +cheap_wait+/+cheap_broadcast+ constructions. See +Node#try_await_lock+. # # Using the first node of a list as a lock does not by itself # suffice though: When a node is locked, any update must first # validate that it is still the first node after locking it, and # retry if not. Because new nodes are always appended to lists, # once a node is first in a bin, it remains first until deleted # or the bin becomes invalidated (upon resizing). However, # operations that only conditionally update may inspect nodes # until the point of update. This is a converse of sorts to the # lazy locking technique described by Herlihy & Shavit. # # The main disadvantage of per-bin locks is that other update # operations on other nodes in a bin list protected by the same # lock can stall, for example when user +eql?+ or mapping # functions take a long time. However, statistically, under # random hash codes, this is not a common problem. Ideally, the # frequency of nodes in bins follows a Poisson distribution # (http://en.wikipedia.org/wiki/Poisson_distribution) with a # parameter of about 0.5 on average, given the resizing threshold # of 0.75, although with a large variance because of resizing # granularity. Ignoring variance, the expected occurrences of # list size k are (exp(-0.5) * pow(0.5, k) / factorial(k)). The # first values are: # # 0: 0.60653066 # 1: 0.30326533 # 2: 0.07581633 # 3: 0.01263606 # 4: 0.00157952 # 5: 0.00015795 # 6: 0.00001316 # 7: 0.00000094 # 8: 0.00000006 # more: less than 1 in ten million # # Lock contention probability for two threads accessing distinct # elements is roughly 1 / (8 * #elements) under random hashes. # # The table is resized when occupancy exceeds a percentage # threshold (nominally, 0.75, but see below). Only a single # thread performs the resize (using field +size_control+, to arrange # exclusion), but the table otherwise remains usable for reads # and updates. Resizing proceeds by transferring bins, one by # one, from the table to the next table. Because we are using # power-of-two expansion, the elements from each bin must either # stay at same index, or move with a power of two offset. We # eliminate unnecessary node creation by catching cases where old # nodes can be reused because their next fields won't change. On # average, only about one-sixth of them need cloning when a table # doubles. The nodes they replace will be garbage collectable as # soon as they are no longer referenced by any reader thread that # may be in the midst of concurrently traversing table. Upon # transfer, the old table bin contains only a special forwarding # node (with hash field +MOVED+) that contains the next table as # its key. On encountering a forwarding node, access and update # operations restart, using the new table. # # Each bin transfer requires its bin lock. However, unlike other # cases, a transfer can skip a bin if it fails to acquire its # lock, and revisit it later. Method +rebuild+ maintains a buffer of # TRANSFER_BUFFER_SIZE bins that have been skipped because of failure # to acquire a lock, and blocks only if none are available # (i.e., only very rarely). The transfer operation must also ensure # that all accessible bins in both the old and new table are usable by # any traversal. When there are no lock acquisition failures, this is # arranged simply by proceeding from the last bin (+table.size - 1+) up # towards the first. Upon seeing a forwarding node, traversals arrange # to move to the new table without revisiting nodes. However, when any # node is skipped during a transfer, all earlier table bins may have # become visible, so are initialized with a reverse-forwarding node back # to the old table until the new ones are established. (This # sometimes requires transiently locking a forwarding node, which # is possible under the above encoding.) These more expensive # mechanics trigger only when necessary. # # The traversal scheme also applies to partial traversals of # ranges of bins (via an alternate Traverser constructor) # to support partitioned aggregate operations. Also, read-only # operations give up if ever forwarded to a null table, which # provides support for shutdown-style clearing, which is also not # currently implemented. # # Lazy table initialization minimizes footprint until first use. # # The element count is maintained using a +ThreadSafe::Util::Adder+, # which avoids contention on updates but can encounter cache thrashing # if read too frequently during concurrent access. To avoid reading so # often, resizing is attempted either when a bin lock is # contended, or upon adding to a bin already holding two or more # nodes (checked before adding in the +x_if_absent+ methods, after # adding in others). Under uniform hash distributions, the # probability of this occurring at threshold is around 13%, # meaning that only about 1 in 8 puts check threshold (and after # resizing, many fewer do so). But this approximation has high # variance for small table sizes, so we check on any collision # for sizes <= 64. The bulk putAll operation further reduces # contention by only committing count updates upon these size # checks. class AtomicReferenceCacheBackend class Table < Util::PowerOfTwoTuple def cas_new_node(i, hash, key, value) cas(i, nil, Node.new(hash, key, value)) end def try_to_cas_in_computed(i, hash, key) succeeded = false new_value = nil new_node = Node.new(locked_hash = hash | LOCKED, key, NULL) if cas(i, nil, new_node) begin if NULL == (new_value = yield(NULL)) was_null = true else new_node.value = new_value end succeeded = true ensure volatile_set(i, nil) if !succeeded || was_null new_node.unlock_via_hash(locked_hash, hash) end end return succeeded, new_value end def try_lock_via_hash(i, node, node_hash) node.try_lock_via_hash(node_hash) do yield if volatile_get(i) == node end end def delete_node_at(i, node, predecessor_node) if predecessor_node predecessor_node.next = node.next else volatile_set(i, node.next) end end end # Key-value entry. Nodes with a hash field of +MOVED+ are special, # and do not contain user keys or values. Otherwise, keys are never +nil+, # and +NULL+ +value+ fields indicate that a node is in the process # of being deleted or created. For purposes of read-only access, a key may be read # before a value, but can only be used after checking value to be +!= NULL+. class Node extend Util::Volatile attr_volatile :hash, :value, :next include Util::CheapLockable bit_shift = Util::FIXNUM_BIT_SIZE - 2 # need 2 bits for ourselves # Encodings for special uses of Node hash fields. See above for explanation. MOVED = ('10' << ('0' * bit_shift)).to_i(2) # hash field for forwarding nodes LOCKED = ('01' << ('0' * bit_shift)).to_i(2) # set/tested only as a bit WAITING = ('11' << ('0' * bit_shift)).to_i(2) # both bits set/tested together HASH_BITS = ('00' << ('1' * bit_shift)).to_i(2) # usable bits of normal node hash SPIN_LOCK_ATTEMPTS = Util::CPU_COUNT > 1 ? Util::CPU_COUNT * 2 : 0 attr_reader :key def initialize(hash, key, value, next_node = nil) super() @key = key self.lazy_set_hash(hash) self.lazy_set_value(value) self.next = next_node end # Spins a while if +LOCKED+ bit set and this node is the first # of its bin, and then sets +WAITING+ bits on hash field and # blocks (once) if they are still set. It is OK for this # method to return even if lock is not available upon exit, # which enables these simple single-wait mechanics. # # The corresponding signalling operation is performed within # callers: Upon detecting that +WAITING+ has been set when # unlocking lock (via a failed CAS from non-waiting +LOCKED+ # state), unlockers acquire the +cheap_synchronize+ lock and # perform a +cheap_broadcast+. def try_await_lock(table, i) if table && i >= 0 && i < table.size # bounds check, TODO: why are we bounds checking? spins = SPIN_LOCK_ATTEMPTS randomizer = base_randomizer = Util::XorShiftRandom.get while equal?(table.volatile_get(i)) && self.class.locked_hash?(my_hash = hash) if spins >= 0 if (randomizer = (randomizer >> 1)).even? # spin at random if (spins -= 1) == 0 Thread.pass # yield before blocking else randomizer = base_randomizer = Util::XorShiftRandom.xorshift(base_randomizer) if randomizer.zero? end end elsif cas_hash(my_hash, my_hash | WAITING) force_aquire_lock(table, i) break end end end end def key?(key) @key.eql?(key) end def matches?(key, hash) pure_hash == hash && key?(key) end def pure_hash hash & HASH_BITS end def try_lock_via_hash(node_hash = hash) if cas_hash(node_hash, locked_hash = node_hash | LOCKED) begin yield ensure unlock_via_hash(locked_hash, node_hash) end end end def locked? self.class.locked_hash?(hash) end def unlock_via_hash(locked_hash, node_hash) unless cas_hash(locked_hash, node_hash) self.hash = node_hash cheap_synchronize { cheap_broadcast } end end private def force_aquire_lock(table, i) cheap_synchronize do if equal?(table.volatile_get(i)) && (hash & WAITING) == WAITING cheap_wait else cheap_broadcast # possibly won race vs signaller end end end class << self def locked_hash?(hash) (hash & LOCKED) != 0 end end end # shorthands MOVED = Node::MOVED LOCKED = Node::LOCKED WAITING = Node::WAITING HASH_BITS = Node::HASH_BITS NOW_RESIZING = -1 DEFAULT_CAPACITY = 16 MAX_CAPACITY = Util::MAX_INT # The buffer size for skipped bins during transfers. The # value is arbitrary but should be large enough to avoid # most locking stalls during resizes. TRANSFER_BUFFER_SIZE = 32 extend Util::Volatile attr_volatile :table, # The array of bins. Lazily initialized upon first insertion. Size is always a power of two. # Table initialization and resizing control. When negative, the # table is being initialized or resized. Otherwise, when table is # null, holds the initial table size to use upon creation, or 0 # for default. After initialization, holds the next element count # value upon which to resize the table. :size_control def initialize(options = nil) super() @counter = Util::Adder.new initial_capacity = options && options[:initial_capacity] || DEFAULT_CAPACITY self.size_control = (capacity = table_size_for(initial_capacity)) > MAX_CAPACITY ? MAX_CAPACITY : capacity end def get_or_default(key, else_value = nil) hash = key_hash(key) current_table = table while current_table node = current_table.volatile_get_by_hash(hash) current_table = while node if (node_hash = node.hash) == MOVED break node.key elsif (node_hash & HASH_BITS) == hash && node.key?(key) && NULL != (value = node.value) return value end node = node.next end end else_value end def [](key) get_or_default(key) end def key?(key) get_or_default(key, NULL) != NULL end def []=(key, value) get_and_set(key, value) value end def compute_if_absent(key) hash = key_hash(key) current_table = table || initialize_table while true if !(node = current_table.volatile_get(i = current_table.hash_to_index(hash))) succeeded, new_value = current_table.try_to_cas_in_computed(i, hash, key) { yield } if succeeded increment_size return new_value end elsif (node_hash = node.hash) == MOVED current_table = node.key elsif NULL != (current_value = find_value_in_node_list(node, key, hash, node_hash & HASH_BITS)) return current_value elsif Node.locked_hash?(node_hash) try_await_lock(current_table, i, node) else succeeded, value = attempt_internal_compute_if_absent(key, hash, current_table, i, node, node_hash) { yield } return value if succeeded end end end def compute_if_present(key) new_value = nil internal_replace(key) do |old_value| if (new_value = yield(NULL == old_value ? nil : old_value)).nil? NULL else new_value end end new_value end def compute(key) internal_compute(key) do |old_value| if (new_value = yield(NULL == old_value ? nil : old_value)).nil? NULL else new_value end end end def merge_pair(key, value) internal_compute(key) do |old_value| if NULL == old_value || !(value = yield(old_value)).nil? value else NULL end end end def replace_pair(key, old_value, new_value) NULL != internal_replace(key, old_value) { new_value } end def replace_if_exists(key, new_value) if (result = internal_replace(key) { new_value }) && NULL != result result end end def get_and_set(key, value) # internalPut in the original CHMV8 hash = key_hash(key) current_table = table || initialize_table while true if !(node = current_table.volatile_get(i = current_table.hash_to_index(hash))) if current_table.cas_new_node(i, hash, key, value) increment_size break end elsif (node_hash = node.hash) == MOVED current_table = node.key elsif Node.locked_hash?(node_hash) try_await_lock(current_table, i, node) else succeeded, old_value = attempt_get_and_set(key, value, hash, current_table, i, node, node_hash) break old_value if succeeded end end end def delete(key) replace_if_exists(key, NULL) end def delete_pair(key, value) result = internal_replace(key, value) { NULL } if result && NULL != result !!result else false end end def each_pair return self unless current_table = table current_table_size = base_size = current_table.size i = base_index = 0 while base_index < base_size if node = current_table.volatile_get(i) if node.hash == MOVED current_table = node.key current_table_size = current_table.size else begin if NULL != (value = node.value) # skip deleted or special nodes yield node.key, value end end while node = node.next end end if (i_with_base = i + base_size) < current_table_size i = i_with_base # visit upper slots if present else i = base_index += 1 end end self end def size (sum = @counter.sum) < 0 ? 0 : sum # ignore transient negative values end def empty? size == 0 end # Implementation for clear. Steps through each bin, removing all nodes. def clear return self unless current_table = table current_table_size = current_table.size deleted_count = i = 0 while i < current_table_size if !(node = current_table.volatile_get(i)) i += 1 elsif (node_hash = node.hash) == MOVED current_table = node.key current_table_size = current_table.size elsif Node.locked_hash?(node_hash) decrement_size(deleted_count) # opportunistically update count deleted_count = 0 node.try_await_lock(current_table, i) else current_table.try_lock_via_hash(i, node, node_hash) do begin deleted_count += 1 if NULL != node.value # recheck under lock node.value = nil end while node = node.next current_table.volatile_set(i, nil) i += 1 end end end decrement_size(deleted_count) self end private # Internal versions of the insertion methods, each a # little more complicated than the last. All have # the same basic structure: # 1. If table uninitialized, create # 2. If bin empty, try to CAS new node # 3. If bin stale, use new table # 4. Lock and validate; if valid, scan and add or update # # The others interweave other checks and/or alternative actions: # * Plain +get_and_set+ checks for and performs resize after insertion. # * compute_if_absent prescans for mapping without lock (and fails to add # if present), which also makes pre-emptive resize checks worthwhile. # # Someday when details settle down a bit more, it might be worth # some factoring to reduce sprawl. def internal_replace(key, expected_old_value = NULL, &block) hash = key_hash(key) current_table = table while current_table if !(node = current_table.volatile_get(i = current_table.hash_to_index(hash))) break elsif (node_hash = node.hash) == MOVED current_table = node.key elsif (node_hash & HASH_BITS) != hash && !node.next # precheck break # rules out possible existence elsif Node.locked_hash?(node_hash) try_await_lock(current_table, i, node) else succeeded, old_value = attempt_internal_replace(key, expected_old_value, hash, current_table, i, node, node_hash, &block) return old_value if succeeded end end NULL end def attempt_internal_replace(key, expected_old_value, hash, current_table, i, node, node_hash) current_table.try_lock_via_hash(i, node, node_hash) do predecessor_node = nil old_value = NULL begin if node.matches?(key, hash) && NULL != (current_value = node.value) if NULL == expected_old_value || expected_old_value == current_value # NULL == expected_old_value means whatever value old_value = current_value if NULL == (node.value = yield(old_value)) current_table.delete_node_at(i, node, predecessor_node) decrement_size end end break end predecessor_node = node end while node = node.next return true, old_value end end def find_value_in_node_list(node, key, hash, pure_hash) do_check_for_resize = false while true if pure_hash == hash && node.key?(key) && NULL != (value = node.value) return value elsif node = node.next do_check_for_resize = true # at least 2 nodes -> check for resize pure_hash = node.pure_hash else return NULL end end ensure check_for_resize if do_check_for_resize end def internal_compute(key, &block) hash = key_hash(key) current_table = table || initialize_table while true if !(node = current_table.volatile_get(i = current_table.hash_to_index(hash))) succeeded, new_value = current_table.try_to_cas_in_computed(i, hash, key, &block) if succeeded if NULL == new_value break nil else increment_size break new_value end end elsif (node_hash = node.hash) == MOVED current_table = node.key elsif Node.locked_hash?(node_hash) try_await_lock(current_table, i, node) else succeeded, new_value = attempt_compute(key, hash, current_table, i, node, node_hash, &block) break new_value if succeeded end end end def attempt_internal_compute_if_absent(key, hash, current_table, i, node, node_hash) added = false current_table.try_lock_via_hash(i, node, node_hash) do while true if node.matches?(key, hash) && NULL != (value = node.value) return true, value end last = node unless node = node.next last.next = Node.new(hash, key, value = yield) added = true increment_size return true, value end end end ensure check_for_resize if added end def attempt_compute(key, hash, current_table, i, node, node_hash) added = false current_table.try_lock_via_hash(i, node, node_hash) do predecessor_node = nil while true if node.matches?(key, hash) && NULL != (value = node.value) if NULL == (node.value = value = yield(value)) current_table.delete_node_at(i, node, predecessor_node) decrement_size value = nil end return true, value end predecessor_node = node unless node = node.next if NULL == (value = yield(NULL)) value = nil else predecessor_node.next = Node.new(hash, key, value) added = true increment_size end return true, value end end end ensure check_for_resize if added end def attempt_get_and_set(key, value, hash, current_table, i, node, node_hash) node_nesting = nil current_table.try_lock_via_hash(i, node, node_hash) do node_nesting = 1 old_value = nil found_old_value = false while node if node.matches?(key, hash) && NULL != (old_value = node.value) found_old_value = true node.value = value break end last = node unless node = node.next last.next = Node.new(hash, key, value) break end node_nesting += 1 end return true, old_value if found_old_value increment_size true end ensure check_for_resize if node_nesting && (node_nesting > 1 || current_table.size <= 64) end def initialize_copy(other) super @counter = Util::Adder.new self.table = nil self.size_control = (other_table = other.table) ? other_table.size : DEFAULT_CAPACITY self end def try_await_lock(current_table, i, node) check_for_resize # try resizing if can't get lock node.try_await_lock(current_table, i) end def key_hash(key) key.hash & HASH_BITS end # Returns a power of two table size for the given desired capacity. def table_size_for(entry_count) size = 2 size <<= 1 while size < entry_count size end # Initializes table, using the size recorded in +size_control+. def initialize_table until current_table ||= table if (size_ctrl = size_control) == NOW_RESIZING Thread.pass # lost initialization race; just spin else try_in_resize_lock(current_table, size_ctrl) do initial_size = size_ctrl > 0 ? size_ctrl : DEFAULT_CAPACITY current_table = self.table = Table.new(initial_size) initial_size - (initial_size >> 2) # 75% load factor end end end current_table end # If table is too small and not already resizing, creates next # table and transfers bins. Rechecks occupancy after a transfer # to see if another resize is already needed because resizings # are lagging additions. def check_for_resize while (current_table = table) && MAX_CAPACITY > (table_size = current_table.size) && NOW_RESIZING != (size_ctrl = size_control) && size_ctrl < @counter.sum try_in_resize_lock(current_table, size_ctrl) do self.table = rebuild(current_table) (table_size << 1) - (table_size >> 1) # 75% load factor end end end def try_in_resize_lock(current_table, size_ctrl) if cas_size_control(size_ctrl, NOW_RESIZING) begin if current_table == table # recheck under lock size_ctrl = yield # get new size_control end ensure self.size_control = size_ctrl end end end # Moves and/or copies the nodes in each bin to new table. See above for explanation. def rebuild(table) old_table_size = table.size new_table = table.next_in_size_table # puts "#{old_table_size} -> #{new_table.size}" forwarder = Node.new(MOVED, new_table, NULL) rev_forwarder = nil locked_indexes = nil # holds bins to revisit; nil until needed locked_arr_idx = 0 bin = old_table_size - 1 i = bin while true if !(node = table.volatile_get(i)) # 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 redo unless (bin >= 0 ? table.cas(i, nil, forwarder) : lock_and_clean_up_reverse_forwarders(table, old_table_size, new_table, i, forwarder)) elsif Node.locked_hash?(node_hash = node.hash) locked_indexes ||= Array.new if bin < 0 && locked_arr_idx > 0 locked_arr_idx -= 1 i, locked_indexes[locked_arr_idx] = locked_indexes[locked_arr_idx], i # swap with another bin redo end if bin < 0 || locked_indexes.size >= TRANSFER_BUFFER_SIZE node.try_await_lock(table, i) # no other options -- block redo end rev_forwarder ||= Node.new(MOVED, table, NULL) redo unless table.volatile_get(i) == node && node.locked? # recheck before adding to list locked_indexes << i new_table.volatile_set(i, rev_forwarder) new_table.volatile_set(i + old_table_size, rev_forwarder) else redo unless split_old_bin(table, new_table, i, node, node_hash, forwarder) end if bin > 0 i = (bin -= 1) elsif locked_indexes && !locked_indexes.empty? bin = -1 i = locked_indexes.pop locked_arr_idx = locked_indexes.size - 1 else return new_table end end end def lock_and_clean_up_reverse_forwarders(old_table, old_table_size, new_table, i, forwarder) # transiently use a locked forwarding node locked_forwarder = Node.new(moved_locked_hash = MOVED | LOCKED, new_table, NULL) if old_table.cas(i, nil, locked_forwarder) new_table.volatile_set(i, nil) # kill the potential reverse forwarders new_table.volatile_set(i + old_table_size, nil) # kill the potential reverse forwarders old_table.volatile_set(i, forwarder) locked_forwarder.unlock_via_hash(moved_locked_hash, MOVED) true end end # Splits a normal bin with list headed by e into lo and hi parts; installs in given table. def split_old_bin(table, new_table, i, node, node_hash, forwarder) table.try_lock_via_hash(i, node, node_hash) do split_bin(new_table, i, node, node_hash) table.volatile_set(i, forwarder) end end def split_bin(new_table, i, node, node_hash) bit = new_table.size >> 1 # bit to split on run_bit = node_hash & bit last_run = nil low = nil high = nil current_node = node # this optimises for the lowest amount of volatile writes and objects created while current_node = current_node.next unless (b = current_node.hash & bit) == run_bit run_bit = b last_run = current_node end end if run_bit == 0 low = last_run else high = last_run end current_node = node until current_node == last_run pure_hash = current_node.pure_hash if (pure_hash & bit) == 0 low = Node.new(pure_hash, current_node.key, current_node.value, low) else high = Node.new(pure_hash, current_node.key, current_node.value, high) end current_node = current_node.next end new_table.volatile_set(i, low) new_table.volatile_set(i + bit, high) end def increment_size @counter.increment end def decrement_size(by = 1) @counter.add(-by) end end end