Class: ThreadSafe::Util::Striped64

Inherits:
Object
  • Object
show all
Extended by:
Volatile
Defined in:
lib/thread_safe/util/striped64.rb

Overview

A Ruby port of the Doug Lea's jsr166e.Striped64 class version 1.6 available in public domain.

Original source code available here: http://gee.cs.oswego.edu/cgi-bin/viewcvs.cgi/jsr166/src/jsr166e/Striped64.java?revision=1.6

Class holding common representation and mechanics for classes supporting dynamic striping on 64bit values.

This class maintains a lazily-initialized table of atomically updated variables, plus an extra +base+ field. The table size is a power of two. Indexing uses masked per-thread hash codes. Nearly all methods on this class are private, accessed directly by subclasses.

Table entries are of class +Cell+; a variant of AtomicLong padded to reduce cache contention on most processors. Padding is overkill for most Atomics because they are usually irregularly scattered in memory and thus don't interfere much with each other. But Atomic objects residing in arrays will tend to be placed adjacent to each other, and so will most often share cache lines (with a huge negative performance impact) without this precaution.

In part because +Cell+s are relatively large, we avoid creating them until they are needed. When there is no contention, all updates are made to the +base+ field. Upon first contention (a failed CAS on +base+ update), the table is initialized to size 2. The table size is doubled upon further contention until reaching the nearest power of two greater than or equal to the number of CPUS. Table slots remain empty (+nil+) until they are needed.

A single spinlock (+busy+) is used for initializing and resizing the table, as well as populating slots with new +Cell+s. There is no need for a blocking lock: When the lock is not available, threads try other slots (or the base). During these retries, there is increased contention and reduced locality, which is still better than alternatives.

Per-thread hash codes are initialized to random values. Contention and/or table collisions are indicated by failed CASes when performing an update operation (see method +retry_update+). Upon a collision, if the table size is less than the capacity, it is doubled in size unless some other thread holds the lock. If a hashed slot is empty, and lock is available, a new +Cell+ is created. Otherwise, if the slot exists, a CAS is tried. Retries proceed by "double hashing", using a secondary hash (XorShift) to try to find a free slot.

The table size is capped because, when there are more threads than CPUs, supposing that each thread were bound to a CPU, there would exist a perfect hash function mapping threads to slots that eliminates collisions. When we reach capacity, we search for this mapping by randomly varying the hash codes of colliding threads. Because search is random, and collisions only become known via CAS failures, convergence can be slow, and because threads are typically not bound to CPUS forever, may not occur at all. However, despite these limitations, observed contention rates are typically low in these cases.

It is possible for a +Cell+ to become unused when threads that once hashed to it terminate, as well as in the case where doubling the table causes no thread to hash to it under expanded mask. We do not try to detect or remove such cells, under the assumption that for long-running instances, observed contention levels will recur, so the cells will eventually be needed again; and for short-lived ones, it does not matter.

Direct Known Subclasses

Adder

Defined Under Namespace

Classes: Cell

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeStriped64

Returns a new instance of Striped64.



89
90
91
92
93
# File 'lib/thread_safe/util/striped64.rb', line 89

def initialize
  super()
  self.busy = false
  self.base = 0
end

Class Method Details

.attr_volatile(*attr_names) ⇒ Object Originally defined in module Volatile

Provides +volatile+ (in the JVM's sense) attribute accessors implemented atop of the +AtomicReference+s.

Usage: class Foo extend ThreadSafe::Util::Volatile attr_volatile :foo, :bar

def initialize(bar)
  super() # must super() into parent initializers before using the volatile attribute accessors
  self.bar = bar
end

def hello
  my_foo = foo # volatile read
  self.foo = 1 # volatile write
  cas_foo(1, 2) # => true | a strong CAS
end

end

Instance Method Details

#retry_update(x, hash_code, was_uncontended) ⇒ Object

Handles cases of updates involving initialization, resizing, creating new Cells, and/or contention. See above for explanation. This method suffers the usual non-modularity problems of optimistic retry code, relying on rechecked sets of reads.

Arguments: [+x+] the value [+hash_code+] hash code used [+x+] false if CAS failed before call



108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# File 'lib/thread_safe/util/striped64.rb', line 108

def retry_update(x, hash_code, was_uncontended) # :yields: current_value
  hash     = hash_code
  collided = false # True if last slot nonempty
  while true
    if current_cells = cells
      if !(cell = current_cells.volatile_get_by_hash(hash))
        if busy?
          collided = false
        else # Try to attach new Cell
          if try_to_install_new_cell(Cell.new(x), hash) # Optimistically create and try to insert new cell
            break
          else
            redo # Slot is now non-empty
          end
        end
      elsif !was_uncontended # CAS already known to fail
        was_uncontended = true # Continue after rehash
      elsif cell.cas_computed {|current_value| yield current_value}
        break
      elsif current_cells.size >= CPU_COUNT || cells != current_cells # At max size or stale
        collided = false
      elsif collided && expand_table_unless_stale(current_cells)
        collided = false
        redo # Retry with expanded table
      else
        collided = true
      end
      hash = XorShiftRandom.xorshift(hash)

    elsif try_initialize_cells(x, hash) || cas_base_computed {|current_base| yield current_base}
      break
    end
  end
  self.hash_code = hash
end