jdantonio/concurrent-ruby

View on GitHub
lib/concurrent-ruby-edge/concurrent/edge/lock_free_linked_set.rb

Summary

Maintainability
A
50 mins
Test Coverage
require 'concurrent/edge/lock_free_linked_set/node'
require 'concurrent/edge/lock_free_linked_set/window'

module Concurrent
  module Edge
    # This class implements a lock-free linked set. The general idea of this
    # implementation is this: each node has a successor which is an Atomic
    # Markable Reference. This is used to ensure that all modifications to the
    # list are atomic, preserving the structure of the linked list under _any_
    # circumstance in a multithreaded application.
    #
    # One interesting aspect of this algorithm occurs with removing a node.
    # Instead of physically removing a node when remove is called, a node is
    # logically removed, by 'marking it.' By doing this, we prevent calls to
    # `remove` from traversing the list twice to perform a physical removal.
    # Instead, we have have calls to `add` and `remove` clean up all marked
    # nodes they encounter while traversing the list.
    #
    # This algorithm is a variation of the Nonblocking Linked Set found in
    # 'The Art of Multiprocessor Programming' by Herlihy and Shavit.
    # @!macro warn.edge
    class LockFreeLinkedSet
      include Enumerable

      # @!macro lock_free_linked_list_method_initialize
      #
      #   @param [Fixnum] initial_size the size of the linked_list to initialize
      def initialize(initial_size = 0, val = nil)
        @head = Head.new

        initial_size.times do
          val = block_given? ? yield : val
          add val
        end
      end

      # @!macro lock_free_linked_list_method_add
      #
      #   Atomically adds the item to the set if it does not yet exist. Note:
      #   internally the set uses `Object#hash` to compare equality of items,
      #   meaning that Strings and other objects will be considered equal
      #   despite being different objects.
      #
      #   @param [Object] item the item you wish to insert
      #
      #   @return [Boolean] `true` if successful. A `false` return indicates
      #   that the item was already in the set.
      def add(item)
        loop do
          window = Window.find @head, item

          pred, curr = window.pred, window.curr

          # Item already in set
          return false if curr == item

          node = Node.new item, curr

          if pred.successor_reference.compare_and_set curr, node, false, false
            return true
          end
        end
      end

      # @!macro lock_free_linked_list_method_<<
      #
      #   Atomically adds the item to the set if it does not yet exist.
      #
      #   @param [Object] item the item you wish to insert
      #
      #   @return [Object] the set on which the :<< method was invoked
      def <<(item)
        add item
        self
      end

      # @!macro lock_free_linked_list_method_contains
      #
      #   Atomically checks to see if the set contains an item. This method
      #   compares equality based on the `Object#hash` method, meaning that the
      #   hashed contents of an object is what determines equality instead of
      #   `Object#object_id`
      #
      #   @param [Object] item the item you to check for presence in the set
      #
      #   @return [Boolean] whether or not the item is in the set
      def contains?(item)
        curr = @head

        while curr < item
          curr = curr.next_node
          marked = curr.successor_reference.marked?
        end

        curr == item && !marked
      end

      # @!macro lock_free_linked_list_method_remove
      #
      #   Atomically attempts to remove an item, comparing using `Object#hash`.
      #
      #   @param [Object] item the item you to remove from the set
      #
      #   @return [Boolean] whether or not the item was removed from the set
      def remove(item)
        loop do
          window = Window.find @head, item
          pred, curr = window.pred, window.curr

          return false if curr != item

          succ = curr.next_node
          removed = curr.successor_reference.compare_and_set succ, succ, false, true

          #next_node unless removed
          next unless removed

          pred.successor_reference.compare_and_set curr, succ, false, false

          return true
        end
      end

      # @!macro lock_free_linked_list_method_each
      #
      #   An iterator to loop through the set.
      #
      #   @yield [item] each item in the set
      #   @yieldparam [Object] item the item you to remove from the set
      #
      #   @return [Object] self: the linked set on which each was called
      def each
        return to_enum unless block_given?

        curr = @head

        until curr.last?
          curr = curr.next_node
          marked = curr.successor_reference.marked?

          yield curr.data unless marked
        end

        self
      end
    end
  end
end