concurrent-ruby
Advanced tools
+4
-0
| ## Current | ||
| ## Release v1.2.3 (16 Jan 2024) | ||
| * See [the GitHub release](https://github.com/ruby-concurrency/concurrent-ruby/releases/tag/v1.2.3) for details. | ||
| ## Release v1.2.2 (24 Feb 2023) | ||
@@ -4,0 +8,0 @@ |
+1
-1
@@ -15,3 +15,3 @@ source 'https://rubygems.org' | ||
| gem 'rake', '~> 13.0' | ||
| gem 'rake-compiler', '~> 1.0', '>= 1.0.7' | ||
| gem 'rake-compiler', '~> 1.0', '>= 1.0.7', '!= 1.2.4' | ||
| gem 'rake-compiler-dock', '~> 1.0' | ||
@@ -18,0 +18,0 @@ gem 'pry', '~> 0.11', platforms: :mri |
@@ -24,5 +24,5 @@ require 'concurrent/utility/engine' | ||
| when Concurrent.on_cruby? | ||
| # Array is thread-safe in practice because CRuby runs | ||
| # threads one at a time and does not do context | ||
| # switching during the execution of C functions. | ||
| # Array is not fully thread-safe on CRuby, see | ||
| # https://github.com/ruby-concurrency/concurrent-ruby/issues/929 | ||
| # So we will need to add synchronization here | ||
| ::Array | ||
@@ -29,0 +29,0 @@ |
@@ -11,65 +11,68 @@ require 'concurrent/collection/map/non_concurrent_map_backend' | ||
| require 'mutex_m' | ||
| include Mutex_m | ||
| # WARNING: Mutex_m is a non-reentrant lock, so the synchronized methods are | ||
| # not allowed to call each other. | ||
| def initialize(*args, &block) | ||
| super | ||
| # WARNING: Mutex is a non-reentrant lock, so the synchronized methods are | ||
| # not allowed to call each other. | ||
| @mutex = Mutex.new | ||
| end | ||
| def [](key) | ||
| synchronize { super } | ||
| @mutex.synchronize { super } | ||
| end | ||
| def []=(key, value) | ||
| synchronize { super } | ||
| @mutex.synchronize { super } | ||
| end | ||
| def compute_if_absent(key) | ||
| synchronize { super } | ||
| @mutex.synchronize { super } | ||
| end | ||
| def compute_if_present(key) | ||
| synchronize { super } | ||
| @mutex.synchronize { super } | ||
| end | ||
| def compute(key) | ||
| synchronize { super } | ||
| @mutex.synchronize { super } | ||
| end | ||
| def merge_pair(key, value) | ||
| synchronize { super } | ||
| @mutex.synchronize { super } | ||
| end | ||
| def replace_pair(key, old_value, new_value) | ||
| synchronize { super } | ||
| @mutex.synchronize { super } | ||
| end | ||
| def replace_if_exists(key, new_value) | ||
| synchronize { super } | ||
| @mutex.synchronize { super } | ||
| end | ||
| def get_and_set(key, value) | ||
| synchronize { super } | ||
| @mutex.synchronize { super } | ||
| end | ||
| def key?(key) | ||
| synchronize { super } | ||
| @mutex.synchronize { super } | ||
| end | ||
| def delete(key) | ||
| synchronize { super } | ||
| @mutex.synchronize { super } | ||
| end | ||
| def delete_pair(key, value) | ||
| synchronize { super } | ||
| @mutex.synchronize { super } | ||
| end | ||
| def clear | ||
| synchronize { super } | ||
| @mutex.synchronize { super } | ||
| end | ||
| def size | ||
| synchronize { super } | ||
| @mutex.synchronize { super } | ||
| end | ||
| def get_or_default(key, default_value) | ||
| synchronize { super } | ||
| @mutex.synchronize { super } | ||
| end | ||
@@ -79,3 +82,3 @@ | ||
| def dupped_backend | ||
| synchronize { super } | ||
| @mutex.synchronize { super } | ||
| end | ||
@@ -82,0 +85,0 @@ end |
@@ -42,2 +42,6 @@ require 'concurrent/utility/engine' | ||
| # @!macro thread_pool_executor_method_active_count | ||
| # The number of threads that are actively executing tasks. | ||
| # @return [Integer] The number of threads that are actively executing tasks. | ||
| # @!macro thread_pool_executor_attr_reader_idletime | ||
@@ -44,0 +48,0 @@ # The number of seconds that a thread may be idle before being reclaimed. |
@@ -91,6 +91,7 @@ require 'concurrent/utility/engine' | ||
| @daemonize = daemonize | ||
| @java_thread_factory = java.util.concurrent.Executors.defaultThreadFactory | ||
| end | ||
| def newThread(runnable) | ||
| thread = java.util.concurrent.Executors.defaultThreadFactory().newThread(runnable) | ||
| thread = @java_thread_factory.newThread(runnable) | ||
| thread.setDaemon(@daemonize) | ||
@@ -97,0 +98,0 @@ return thread |
@@ -76,2 +76,7 @@ if Concurrent.on_jruby? | ||
| # @!macro thread_pool_executor_method_active_count | ||
| def active_count | ||
| @executor.getActiveCount | ||
| end | ||
| # @!macro thread_pool_executor_attr_reader_idletime | ||
@@ -78,0 +83,0 @@ def idletime |
@@ -64,2 +64,9 @@ require 'thread' | ||
| # @!macro thread_pool_executor_method_active_count | ||
| def active_count | ||
| synchronize do | ||
| @pool.length - @ready.length | ||
| end | ||
| end | ||
| # @!macro executor_service_method_can_overflow_question | ||
@@ -66,0 +73,0 @@ def can_overflow? |
@@ -6,3 +6,3 @@ require 'concurrent/scheduled_task' | ||
| require 'concurrent/executor/single_thread_executor' | ||
| require 'concurrent/errors' | ||
| require 'concurrent/options' | ||
@@ -166,3 +166,7 @@ | ||
| task = synchronize { @queue.pop } | ||
| task.executor.post { task.process_task } | ||
| begin | ||
| task.executor.post { task.process_task } | ||
| rescue RejectedExecutionError | ||
| # ignore and continue | ||
| end | ||
| else | ||
@@ -169,0 +173,0 @@ @condition.wait([diff, 60].min) |
@@ -18,5 +18,7 @@ require 'concurrent/utility/engine' | ||
| when Concurrent.on_cruby? | ||
| # Hash is thread-safe in practice because CRuby runs | ||
| # threads one at a time and does not do context | ||
| # switching during the execution of C functions. | ||
| # Hash is not fully thread-safe on CRuby, see | ||
| # https://bugs.ruby-lang.org/issues/19237 | ||
| # https://github.com/ruby/ruby/commit/ffd52412ab | ||
| # https://github.com/ruby-concurrency/concurrent-ruby/issues/929 | ||
| # So we will need to add synchronization here (similar to Concurrent::Map). | ||
| ::Hash | ||
@@ -23,0 +25,0 @@ |
@@ -23,4 +23,4 @@ require 'thread' | ||
| else | ||
| require 'concurrent/collection/map/atomic_reference_map_backend' | ||
| AtomicReferenceMapBackend | ||
| require 'concurrent/collection/map/synchronized_map_backend' | ||
| SynchronizedMapBackend | ||
| end | ||
@@ -27,0 +27,0 @@ else |
@@ -35,2 +35,13 @@ require 'concurrent/collection/copy_on_notify_observer_set' | ||
| # | ||
| # A `TimerTask` supports two different types of interval calculations. | ||
| # A fixed delay will always wait the same amount of time between the | ||
| # completion of one task and the start of the next. A fixed rate will | ||
| # attempt to maintain a constant rate of execution regardless of the | ||
| # duration of the task. For example, if a fixed rate task is scheduled | ||
| # to run every 60 seconds but the task itself takes 10 seconds to | ||
| # complete, the next task will be scheduled to run 50 seconds after | ||
| # the start of the previous task. If the task takes 70 seconds to | ||
| # complete, the next task will be start immediately after the previous | ||
| # task completes. Tasks will not be executed concurrently. | ||
| # | ||
| # In some cases it may be necessary for a `TimerTask` to affect its own | ||
@@ -78,2 +89,8 @@ # execution cycle. To facilitate this, a reference to the TimerTask instance | ||
| # | ||
| # @example Configuring `:interval_type` with either :fixed_delay or :fixed_rate, default is :fixed_delay | ||
| # task = Concurrent::TimerTask.new(execution_interval: 5, interval_type: :fixed_rate) do | ||
| # puts 'Boom!' | ||
| # end | ||
| # task.interval_type #=> :fixed_rate | ||
| # | ||
| # @example Last `#value` and `Dereferenceable` mixin | ||
@@ -92,3 +109,3 @@ # task = Concurrent::TimerTask.new( | ||
| # timer_task = Concurrent::TimerTask.new(execution_interval: 1) do |task| | ||
| # task.execution_interval.times{ print 'Boom! ' } | ||
| # task.execution_interval.to_i.times{ print 'Boom! ' } | ||
| # print "\n" | ||
@@ -102,3 +119,3 @@ # task.execution_interval += 1 | ||
| # | ||
| # timer_task.execute # blocking call - this task will stop itself | ||
| # timer_task.execute | ||
| # #=> Boom! | ||
@@ -159,5 +176,13 @@ # #=> Boom! Boom! | ||
| # Default `:timeout_interval` in seconds. | ||
| TIMEOUT_INTERVAL = 30 | ||
| # Maintain the interval between the end of one execution and the start of the next execution. | ||
| FIXED_DELAY = :fixed_delay | ||
| # Maintain the interval between the start of one execution and the start of the next. | ||
| # If execution time exceeds the interval, the next execution will start immediately | ||
| # after the previous execution finishes. Executions will not run concurrently. | ||
| FIXED_RATE = :fixed_rate | ||
| # Default `:interval_type` | ||
| DEFAULT_INTERVAL_TYPE = FIXED_DELAY | ||
| # Create a new TimerTask with the given task and configuration. | ||
@@ -167,3 +192,3 @@ # | ||
| # @param [Hash] opts the options defining task execution. | ||
| # @option opts [Integer] :execution_interval number of seconds between | ||
| # @option opts [Float] :execution_interval number of seconds between | ||
| # task executions (default: EXECUTION_INTERVAL) | ||
@@ -173,2 +198,6 @@ # @option opts [Boolean] :run_now Whether to run the task immediately | ||
| # has passed (default: false) | ||
| # @options opts [Symbol] :interval_type method to calculate the interval | ||
| # between executions, can be either :fixed_rate or :fixed_delay. | ||
| # (default: :fixed_delay) | ||
| # @option opts [Executor] executor, default is `global_io_executor` | ||
| # | ||
@@ -252,2 +281,6 @@ # @!macro deref_options | ||
| # @!attribute [r] interval_type | ||
| # @return [Symbol] method to calculate the interval between executions | ||
| attr_reader :interval_type | ||
| # @!attribute [rw] timeout_interval | ||
@@ -275,7 +308,13 @@ # @return [Fixnum] Number of seconds the task can run before it is | ||
| self.execution_interval = opts[:execution] || opts[:execution_interval] || EXECUTION_INTERVAL | ||
| if opts[:interval_type] && ![FIXED_DELAY, FIXED_RATE].include?(opts[:interval_type]) | ||
| raise ArgumentError.new('interval_type must be either :fixed_delay or :fixed_rate') | ||
| end | ||
| if opts[:timeout] || opts[:timeout_interval] | ||
| warn 'TimeTask timeouts are now ignored as these were not able to be implemented correctly' | ||
| end | ||
| @run_now = opts[:now] || opts[:run_now] | ||
| @executor = Concurrent::SafeTaskExecutor.new(task) | ||
| @interval_type = opts[:interval_type] || DEFAULT_INTERVAL_TYPE | ||
| @task = Concurrent::SafeTaskExecutor.new(task) | ||
| @executor = opts[:executor] || Concurrent.global_io_executor | ||
| @running = Concurrent::AtomicBoolean.new(false) | ||
@@ -301,3 +340,3 @@ @value = nil | ||
| def schedule_next_task(interval = execution_interval) | ||
| ScheduledTask.execute(interval, args: [Concurrent::Event.new], &method(:execute_task)) | ||
| ScheduledTask.execute(interval, executor: @executor, args: [Concurrent::Event.new], &method(:execute_task)) | ||
| nil | ||
@@ -309,6 +348,7 @@ end | ||
| return nil unless @running.true? | ||
| _success, value, reason = @executor.execute(self) | ||
| start_time = Concurrent.monotonic_time | ||
| _success, value, reason = @task.execute(self) | ||
| if completion.try? | ||
| self.value = value | ||
| schedule_next_task | ||
| schedule_next_task(calculate_next_interval(start_time)) | ||
| time = Time.now | ||
@@ -321,3 +361,13 @@ observers.notify_observers do | ||
| end | ||
| # @!visibility private | ||
| def calculate_next_interval(start_time) | ||
| if @interval_type == FIXED_RATE | ||
| run_time = Concurrent.monotonic_time - start_time | ||
| [execution_interval - run_time, 0].max | ||
| else # FIXED_DELAY | ||
| execution_interval | ||
| end | ||
| end | ||
| end | ||
| end |
| module Concurrent | ||
| VERSION = '1.2.2' | ||
| VERSION = '1.2.3' | ||
| end |
+2
-0
@@ -378,2 +378,4 @@ # Concurrent Ruby | ||
| * [Rafael França](https://github.com/rafaelfranca) | ||
| * [Charles Oliver Nutter](https://github.com/headius) | ||
| * [Ben Sheldon](https://github.com/bensheldon) | ||
| * [Samuel Williams](https://github.com/ioquatix) | ||
@@ -380,0 +382,0 @@ |
| require 'concurrent/constants' | ||
| require 'concurrent/thread_safe/util' | ||
| require 'concurrent/thread_safe/util/adder' | ||
| require 'concurrent/thread_safe/util/cheap_lockable' | ||
| require 'concurrent/thread_safe/util/power_of_two_tuple' | ||
| require 'concurrent/thread_safe/util/volatile' | ||
| require 'concurrent/thread_safe/util/xor_shift_random' | ||
| module Concurrent | ||
| # @!visibility private | ||
| module Collection | ||
| # 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 +Concurrent::ThreadSafe::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 +Concurrent::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. | ||
| # | ||
| # @!visibility private | ||
| class AtomicReferenceMapBackend | ||
| # @!visibility private | ||
| class Table < Concurrent::ThreadSafe::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+. | ||
| # | ||
| # @!visibility private | ||
| class Node | ||
| extend Concurrent::ThreadSafe::Util::Volatile | ||
| attr_volatile :hash, :value, :next | ||
| include Concurrent::ThreadSafe::Util::CheapLockable | ||
| bit_shift = Concurrent::ThreadSafe::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 = Concurrent::ThreadSafe::Util::CPU_COUNT > 1 ? Concurrent::ThreadSafe::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 = Concurrent::ThreadSafe::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 = Concurrent::ThreadSafe::Util::XorShiftRandom.xorshift(base_randomizer) if randomizer.zero? | ||
| end | ||
| end | ||
| elsif cas_hash(my_hash, my_hash | WAITING) | ||
| force_acquire_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_acquire_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 = Concurrent::ThreadSafe::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 Concurrent::ThreadSafe::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 = Concurrent::ThreadSafe::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 = Concurrent::ThreadSafe::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 | ||
| end |
| require 'concurrent/thread_safe/util' | ||
| require 'concurrent/thread_safe/util/volatile' | ||
| require 'concurrent/utility/engine' | ||
| module Concurrent | ||
| # @!visibility private | ||
| module ThreadSafe | ||
| # @!visibility private | ||
| module Util | ||
| # Provides a cheapest possible (mainly in terms of memory usage) +Mutex+ | ||
| # with the +ConditionVariable+ bundled in. | ||
| # | ||
| # Usage: | ||
| # class A | ||
| # include CheapLockable | ||
| # | ||
| # def do_exlusively | ||
| # cheap_synchronize { yield } | ||
| # end | ||
| # | ||
| # def wait_for_something | ||
| # cheap_synchronize do | ||
| # cheap_wait until resource_available? | ||
| # do_something | ||
| # cheap_broadcast # wake up others | ||
| # end | ||
| # end | ||
| # end | ||
| # | ||
| # @!visibility private | ||
| module CheapLockable | ||
| private | ||
| if Concurrent.on_jruby? | ||
| # Use Java's native synchronized (this) { wait(); notifyAll(); } to avoid the overhead of the extra Mutex objects | ||
| require 'jruby' | ||
| def cheap_synchronize | ||
| JRuby.reference0(self).synchronized { yield } | ||
| end | ||
| def cheap_wait | ||
| JRuby.reference0(self).wait | ||
| end | ||
| def cheap_broadcast | ||
| JRuby.reference0(self).notify_all | ||
| end | ||
| else | ||
| require 'thread' | ||
| extend Volatile | ||
| attr_volatile :mutex | ||
| # Non-reentrant Mutex#syncrhonize | ||
| def cheap_synchronize | ||
| true until (my_mutex = mutex) || cas_mutex(nil, my_mutex = Mutex.new) | ||
| my_mutex.synchronize { yield } | ||
| end | ||
| # Releases this object's +cheap_synchronize+ lock and goes to sleep waiting for other threads to +cheap_broadcast+, reacquires the lock on wakeup. | ||
| # Must only be called in +cheap_broadcast+'s block. | ||
| def cheap_wait | ||
| conditional_variable = @conditional_variable ||= ConditionVariable.new | ||
| conditional_variable.wait(mutex) | ||
| end | ||
| # Wakes up all threads waiting for this object's +cheap_synchronize+ lock. | ||
| # Must only be called in +cheap_broadcast+'s block. | ||
| def cheap_broadcast | ||
| if conditional_variable = @conditional_variable | ||
| conditional_variable.broadcast | ||
| end | ||
| end | ||
| end | ||
| end | ||
| end | ||
| end | ||
| end |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display