Add bin and edit workflow
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 9s

This commit is contained in:
2026-09-16 13:11:16 -06:00
parent c8ac4fcae5
commit 4cee170d66
17576 changed files with 895740 additions and 2 deletions
@@ -0,0 +1,127 @@
require 'concurrent/utility/native_extension_loader' # load native parts first
require 'concurrent/atomic/mutex_atomic_boolean'
module Concurrent
###################################################################
# @!macro atomic_boolean_method_initialize
#
# Creates a new `AtomicBoolean` with the given initial value.
#
# @param [Boolean] initial the initial value
# @!macro atomic_boolean_method_value_get
#
# Retrieves the current `Boolean` value.
#
# @return [Boolean] the current value
# @!macro atomic_boolean_method_value_set
#
# Explicitly sets the value.
#
# @param [Boolean] value the new value to be set
#
# @return [Boolean] the current value
# @!macro atomic_boolean_method_true_question
#
# Is the current value `true`
#
# @return [Boolean] true if the current value is `true`, else false
# @!macro atomic_boolean_method_false_question
#
# Is the current value `false`
#
# @return [Boolean] true if the current value is `false`, else false
# @!macro atomic_boolean_method_make_true
#
# Explicitly sets the value to true.
#
# @return [Boolean] true if value has changed, otherwise false
# @!macro atomic_boolean_method_make_false
#
# Explicitly sets the value to false.
#
# @return [Boolean] true if value has changed, otherwise false
###################################################################
# @!macro atomic_boolean_public_api
#
# @!method initialize(initial = false)
# @!macro atomic_boolean_method_initialize
#
# @!method value
# @!macro atomic_boolean_method_value_get
#
# @!method value=(value)
# @!macro atomic_boolean_method_value_set
#
# @!method true?
# @!macro atomic_boolean_method_true_question
#
# @!method false?
# @!macro atomic_boolean_method_false_question
#
# @!method make_true
# @!macro atomic_boolean_method_make_true
#
# @!method make_false
# @!macro atomic_boolean_method_make_false
###################################################################
# @!visibility private
# @!macro internal_implementation_note
AtomicBooleanImplementation = case
when Concurrent.on_cruby? && Concurrent.c_extensions_loaded?
CAtomicBoolean
when Concurrent.on_jruby?
JavaAtomicBoolean
else
MutexAtomicBoolean
end
private_constant :AtomicBooleanImplementation
# @!macro atomic_boolean
#
# A boolean value that can be updated atomically. Reads and writes to an atomic
# boolean and thread-safe and guaranteed to succeed. Reads and writes may block
# briefly but no explicit locking is required.
#
# @!macro thread_safe_variable_comparison
#
# Performance:
#
# ```
# Testing with ruby 2.1.2
# Testing with Concurrent::MutexAtomicBoolean...
# 2.790000 0.000000 2.790000 ( 2.791454)
# Testing with Concurrent::CAtomicBoolean...
# 0.740000 0.000000 0.740000 ( 0.740206)
#
# Testing with jruby 1.9.3
# Testing with Concurrent::MutexAtomicBoolean...
# 5.240000 2.520000 7.760000 ( 3.683000)
# Testing with Concurrent::JavaAtomicBoolean...
# 3.340000 0.010000 3.350000 ( 0.855000)
# ```
#
# @see http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/atomic/AtomicBoolean.html java.util.concurrent.atomic.AtomicBoolean
#
# @!macro atomic_boolean_public_api
class AtomicBoolean < AtomicBooleanImplementation
# @return [String] Short string representation.
def to_s
format '%s value:%s>', super[0..-2], value
end
alias_method :inspect, :to_s
end
end
@@ -0,0 +1,144 @@
require 'concurrent/utility/native_extension_loader' # load native parts first
require 'concurrent/atomic/mutex_atomic_fixnum'
module Concurrent
###################################################################
# @!macro atomic_fixnum_method_initialize
#
# Creates a new `AtomicFixnum` with the given initial value.
#
# @param [Fixnum] initial the initial value
# @raise [ArgumentError] if the initial value is not a `Fixnum`
# @!macro atomic_fixnum_method_value_get
#
# Retrieves the current `Fixnum` value.
#
# @return [Fixnum] the current value
# @!macro atomic_fixnum_method_value_set
#
# Explicitly sets the value.
#
# @param [Fixnum] value the new value to be set
#
# @return [Fixnum] the current value
#
# @raise [ArgumentError] if the new value is not a `Fixnum`
# @!macro atomic_fixnum_method_increment
#
# Increases the current value by the given amount (defaults to 1).
#
# @param [Fixnum] delta the amount by which to increase the current value
#
# @return [Fixnum] the current value after incrementation
# @!macro atomic_fixnum_method_decrement
#
# Decreases the current value by the given amount (defaults to 1).
#
# @param [Fixnum] delta the amount by which to decrease the current value
#
# @return [Fixnum] the current value after decrementation
# @!macro atomic_fixnum_method_compare_and_set
#
# Atomically sets the value to the given updated value if the current
# value == the expected value.
#
# @param [Fixnum] expect the expected value
# @param [Fixnum] update the new value
#
# @return [Boolean] true if the value was updated else false
# @!macro atomic_fixnum_method_update
#
# Pass the current value to the given block, replacing it
# with the block's result. May retry if the value changes
# during the block's execution.
#
# @yield [Object] Calculate a new value for the atomic reference using
# given (old) value
# @yieldparam [Object] old_value the starting value of the atomic reference
#
# @return [Object] the new value
###################################################################
# @!macro atomic_fixnum_public_api
#
# @!method initialize(initial = 0)
# @!macro atomic_fixnum_method_initialize
#
# @!method value
# @!macro atomic_fixnum_method_value_get
#
# @!method value=(value)
# @!macro atomic_fixnum_method_value_set
#
# @!method increment(delta = 1)
# @!macro atomic_fixnum_method_increment
#
# @!method decrement(delta = 1)
# @!macro atomic_fixnum_method_decrement
#
# @!method compare_and_set(expect, update)
# @!macro atomic_fixnum_method_compare_and_set
#
# @!method update
# @!macro atomic_fixnum_method_update
###################################################################
# @!visibility private
# @!macro internal_implementation_note
AtomicFixnumImplementation = case
when Concurrent.on_cruby? && Concurrent.c_extensions_loaded?
CAtomicFixnum
when Concurrent.on_jruby?
JavaAtomicFixnum
else
MutexAtomicFixnum
end
private_constant :AtomicFixnumImplementation
# @!macro atomic_fixnum
#
# A numeric value that can be updated atomically. Reads and writes to an atomic
# fixnum and thread-safe and guaranteed to succeed. Reads and writes may block
# briefly but no explicit locking is required.
#
# @!macro thread_safe_variable_comparison
#
# Performance:
#
# ```
# Testing with ruby 2.1.2
# Testing with Concurrent::MutexAtomicFixnum...
# 3.130000 0.000000 3.130000 ( 3.136505)
# Testing with Concurrent::CAtomicFixnum...
# 0.790000 0.000000 0.790000 ( 0.785550)
#
# Testing with jruby 1.9.3
# Testing with Concurrent::MutexAtomicFixnum...
# 5.460000 2.460000 7.920000 ( 3.715000)
# Testing with Concurrent::JavaAtomicFixnum...
# 4.520000 0.030000 4.550000 ( 1.187000)
# ```
#
# @see http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/atomic/AtomicLong.html java.util.concurrent.atomic.AtomicLong
#
# @!macro atomic_fixnum_public_api
class AtomicFixnum < AtomicFixnumImplementation
# @return [String] Short string representation.
def to_s
format '%s value:%s>', super[0..-2], value
end
alias_method :inspect, :to_s
end
end
@@ -0,0 +1,167 @@
require 'concurrent/errors'
require 'concurrent/synchronization/object'
module Concurrent
# An atomic reference which maintains an object reference along with a mark bit
# that can be updated atomically.
#
# @see http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/atomic/AtomicMarkableReference.html
# java.util.concurrent.atomic.AtomicMarkableReference
class AtomicMarkableReference < ::Concurrent::Synchronization::Object
attr_atomic(:reference)
private :reference, :reference=, :swap_reference, :compare_and_set_reference, :update_reference
def initialize(value = nil, mark = false)
super()
self.reference = immutable_array(value, mark)
end
# Atomically sets the value and mark to the given updated value and
# mark given both:
# - the current value == the expected value &&
# - the current mark == the expected mark
#
# @param [Object] expected_val the expected value
# @param [Object] new_val the new value
# @param [Boolean] expected_mark the expected mark
# @param [Boolean] new_mark the new mark
#
# @return [Boolean] `true` if successful. A `false` return indicates
# that the actual value was not equal to the expected value or the
# actual mark was not equal to the expected mark
def compare_and_set(expected_val, new_val, expected_mark, new_mark)
# Memoize a valid reference to the current AtomicReference for
# later comparison.
current = reference
curr_val, curr_mark = current
# Ensure that that the expected marks match.
return false unless expected_mark == curr_mark
if expected_val.is_a? Numeric
# If the object is a numeric, we need to ensure we are comparing
# the numerical values
return false unless expected_val == curr_val
else
# Otherwise, we need to ensure we are comparing the object identity.
# Theoretically, this could be incorrect if a user monkey-patched
# `Object#equal?`, but they should know that they are playing with
# fire at that point.
return false unless expected_val.equal? curr_val
end
prospect = immutable_array(new_val, new_mark)
compare_and_set_reference current, prospect
end
alias_method :compare_and_swap, :compare_and_set
# Gets the current reference and marked values.
#
# @return [Array] the current reference and marked values
def get
reference
end
# Gets the current value of the reference
#
# @return [Object] the current value of the reference
def value
reference[0]
end
# Gets the current marked value
#
# @return [Boolean] the current marked value
def mark
reference[1]
end
alias_method :marked?, :mark
# _Unconditionally_ sets to the given value of both the reference and
# the mark.
#
# @param [Object] new_val the new value
# @param [Boolean] new_mark the new mark
#
# @return [Array] both the new value and the new mark
def set(new_val, new_mark)
self.reference = immutable_array(new_val, new_mark)
end
# Pass the current value and marked state to the given block, replacing it
# with the block's results. May retry if the value changes during the
# block's execution.
#
# @yield [Object] Calculate a new value and marked state for the atomic
# reference using given (old) value and (old) marked
# @yieldparam [Object] old_val the starting value of the atomic reference
# @yieldparam [Boolean] old_mark the starting state of marked
#
# @return [Array] the new value and new mark
def update
loop do
old_val, old_mark = reference
new_val, new_mark = yield old_val, old_mark
if compare_and_set old_val, new_val, old_mark, new_mark
return immutable_array(new_val, new_mark)
end
end
end
# Pass the current value to the given block, replacing it
# with the block's result. Raise an exception if the update
# fails.
#
# @yield [Object] Calculate a new value and marked state for the atomic
# reference using given (old) value and (old) marked
# @yieldparam [Object] old_val the starting value of the atomic reference
# @yieldparam [Boolean] old_mark the starting state of marked
#
# @return [Array] the new value and marked state
#
# @raise [Concurrent::ConcurrentUpdateError] if the update fails
def try_update!
old_val, old_mark = reference
new_val, new_mark = yield old_val, old_mark
unless compare_and_set old_val, new_val, old_mark, new_mark
fail ::Concurrent::ConcurrentUpdateError,
'AtomicMarkableReference: Update failed due to race condition.',
'Note: If you would like to guarantee an update, please use ' +
'the `AtomicMarkableReference#update` method.'
end
immutable_array(new_val, new_mark)
end
# Pass the current value to the given block, replacing it with the
# block's result. Simply return nil if update fails.
#
# @yield [Object] Calculate a new value and marked state for the atomic
# reference using given (old) value and (old) marked
# @yieldparam [Object] old_val the starting value of the atomic reference
# @yieldparam [Boolean] old_mark the starting state of marked
#
# @return [Array] the new value and marked state, or nil if
# the update failed
def try_update
old_val, old_mark = reference
new_val, new_mark = yield old_val, old_mark
return unless compare_and_set old_val, new_val, old_mark, new_mark
immutable_array(new_val, new_mark)
end
private
def immutable_array(*args)
args.freeze
end
end
end
@@ -0,0 +1,135 @@
require 'concurrent/utility/native_extension_loader' # load native parts first
require 'concurrent/atomic_reference/atomic_direct_update'
require 'concurrent/atomic_reference/numeric_cas_wrapper'
require 'concurrent/atomic_reference/mutex_atomic'
# Shim for TruffleRuby::AtomicReference
if Concurrent.on_truffleruby? && !defined?(TruffleRuby::AtomicReference)
# @!visibility private
module TruffleRuby
AtomicReference = Truffle::AtomicReference
end
end
module Concurrent
# @!macro internal_implementation_note
AtomicReferenceImplementation = case
when Concurrent.on_cruby? && Concurrent.c_extensions_loaded?
# @!visibility private
# @!macro internal_implementation_note
class CAtomicReference
include AtomicDirectUpdate
include AtomicNumericCompareAndSetWrapper
alias_method :compare_and_swap, :compare_and_set
end
CAtomicReference
when Concurrent.on_jruby?
# @!visibility private
# @!macro internal_implementation_note
class JavaAtomicReference
include AtomicDirectUpdate
end
JavaAtomicReference
when Concurrent.on_truffleruby?
class TruffleRubyAtomicReference < TruffleRuby::AtomicReference
include AtomicDirectUpdate
alias_method :value, :get
alias_method :value=, :set
alias_method :compare_and_swap, :compare_and_set
alias_method :swap, :get_and_set
end
TruffleRubyAtomicReference
else
MutexAtomicReference
end
private_constant :AtomicReferenceImplementation
# An object reference that may be updated atomically. All read and write
# operations have java volatile semantic.
#
# @!macro thread_safe_variable_comparison
#
# @see http://docs.oracle.com/javase/8/docs/api/java/util/concurrent/atomic/AtomicReference.html
# @see http://docs.oracle.com/javase/8/docs/api/java/util/concurrent/atomic/package-summary.html
#
# @!method initialize(value = nil)
# @!macro atomic_reference_method_initialize
# @param [Object] value The initial value.
#
# @!method get
# @!macro atomic_reference_method_get
# Gets the current value.
# @return [Object] the current value
#
# @!method set(new_value)
# @!macro atomic_reference_method_set
# Sets to the given value.
# @param [Object] new_value the new value
# @return [Object] the new value
#
# @!method get_and_set(new_value)
# @!macro atomic_reference_method_get_and_set
# Atomically sets to the given value and returns the old value.
# @param [Object] new_value the new value
# @return [Object] the old value
#
# @!method compare_and_set(old_value, new_value)
# @!macro atomic_reference_method_compare_and_set
#
# Atomically sets the value to the given updated value if
# the current value == the expected value.
#
# @param [Object] old_value the expected value
# @param [Object] new_value the new value
#
# @return [Boolean] `true` if successful. A `false` return indicates
# that the actual value was not equal to the expected value.
#
# @!method update
# Pass the current value to the given block, replacing it
# with the block's result. May retry if the value changes
# during the block's execution.
#
# @yield [Object] Calculate a new value for the atomic reference using
# given (old) value
# @yieldparam [Object] old_value the starting value of the atomic reference
# @return [Object] the new value
#
# @!method try_update
# Pass the current value to the given block, replacing it
# with the block's result. Return nil if the update fails.
#
# @yield [Object] Calculate a new value for the atomic reference using
# given (old) value
# @yieldparam [Object] old_value the starting value of the atomic reference
# @note This method was altered to avoid raising an exception by default.
# Instead, this method now returns `nil` in case of failure. For more info,
# please see: https://github.com/ruby-concurrency/concurrent-ruby/pull/336
# @return [Object] the new value, or nil if update failed
#
# @!method try_update!
# Pass the current value to the given block, replacing it
# with the block's result. Raise an exception if the update
# fails.
#
# @yield [Object] Calculate a new value for the atomic reference using
# given (old) value
# @yieldparam [Object] old_value the starting value of the atomic reference
# @note This behavior mimics the behavior of the original
# `AtomicReference#try_update` API. The reason this was changed was to
# avoid raising exceptions (which are inherently slow) by default. For more
# info: https://github.com/ruby-concurrency/concurrent-ruby/pull/336
# @return [Object] the new value
# @raise [Concurrent::ConcurrentUpdateError] if the update fails
class AtomicReference < AtomicReferenceImplementation
# @return [String] Short string representation.
def to_s
format '%s value:%s>', super[0..-2], get
end
alias_method :inspect, :to_s
end
end
@@ -0,0 +1,100 @@
require 'concurrent/utility/engine'
require 'concurrent/atomic/mutex_count_down_latch'
require 'concurrent/atomic/java_count_down_latch'
module Concurrent
###################################################################
# @!macro count_down_latch_method_initialize
#
# Create a new `CountDownLatch` with the initial `count`.
#
# @param [new] count the initial count
#
# @raise [ArgumentError] if `count` is not an integer or is less than zero
# @!macro count_down_latch_method_wait
#
# Block on the latch until the counter reaches zero or until `timeout` is reached.
#
# @param [Fixnum] timeout the number of seconds to wait for the counter or `nil`
# to block indefinitely
# @return [Boolean] `true` if the `count` reaches zero else false on `timeout`
# @!macro count_down_latch_method_count_down
#
# Signal the latch to decrement the counter. Will signal all blocked threads when
# the `count` reaches zero.
# @!macro count_down_latch_method_count
#
# The current value of the counter.
#
# @return [Fixnum] the current value of the counter
###################################################################
# @!macro count_down_latch_public_api
#
# @!method initialize(count = 1)
# @!macro count_down_latch_method_initialize
#
# @!method wait(timeout = nil)
# @!macro count_down_latch_method_wait
#
# @!method count_down
# @!macro count_down_latch_method_count_down
#
# @!method count
# @!macro count_down_latch_method_count
###################################################################
# @!visibility private
# @!macro internal_implementation_note
CountDownLatchImplementation = case
when Concurrent.on_jruby?
JavaCountDownLatch
else
MutexCountDownLatch
end
private_constant :CountDownLatchImplementation
# @!macro count_down_latch
#
# A synchronization object that allows one thread to wait on multiple other threads.
# The thread that will wait creates a `CountDownLatch` and sets the initial value
# (normally equal to the number of other threads). The initiating thread passes the
# latch to the other threads then waits for the other threads by calling the `#wait`
# method. Each of the other threads calls `#count_down` when done with its work.
# When the latch counter reaches zero the waiting thread is unblocked and continues
# with its work. A `CountDownLatch` can be used only once. Its value cannot be reset.
#
# @!macro count_down_latch_public_api
# @example Waiter and Decrementer
# latch = Concurrent::CountDownLatch.new(3)
#
# waiter = Thread.new do
# latch.wait()
# puts ("Waiter released")
# end
#
# decrementer = Thread.new do
# sleep(1)
# latch.count_down
# puts latch.count
#
# sleep(1)
# latch.count_down
# puts latch.count
#
# sleep(1)
# latch.count_down
# puts latch.count
# end
#
# [waiter, decrementer].each(&:join)
class CountDownLatch < CountDownLatchImplementation
end
end
@@ -0,0 +1,128 @@
require 'concurrent/synchronization/lockable_object'
require 'concurrent/utility/native_integer'
module Concurrent
# A synchronization aid that allows a set of threads to all wait for each
# other to reach a common barrier point.
# @example
# barrier = Concurrent::CyclicBarrier.new(3)
# jobs = Array.new(3) { |i| -> { sleep i; p done: i } }
# process = -> (i) do
# # waiting to start at the same time
# barrier.wait
# # execute job
# jobs[i].call
# # wait for others to finish
# barrier.wait
# end
# threads = 2.times.map do |i|
# Thread.new(i, &process)
# end
#
# # use main as well
# process.call 2
#
# # here we can be sure that all jobs are processed
class CyclicBarrier < Synchronization::LockableObject
# @!visibility private
Generation = Struct.new(:status)
private_constant :Generation
# Create a new `CyclicBarrier` that waits for `parties` threads
#
# @param [Fixnum] parties the number of parties
# @yield an optional block that will be executed that will be executed after
# the last thread arrives and before the others are released
#
# @raise [ArgumentError] if `parties` is not an integer or is less than zero
def initialize(parties, &block)
Utility::NativeInteger.ensure_integer_and_bounds parties
Utility::NativeInteger.ensure_positive_and_no_zero parties
super(&nil)
synchronize { ns_initialize parties, &block }
end
# @return [Fixnum] the number of threads needed to pass the barrier
def parties
synchronize { @parties }
end
# @return [Fixnum] the number of threads currently waiting on the barrier
def number_waiting
synchronize { @number_waiting }
end
# Blocks on the barrier until the number of waiting threads is equal to
# `parties` or until `timeout` is reached or `reset` is called
# If a block has been passed to the constructor, it will be executed once by
# the last arrived thread before releasing the others
# @param [Fixnum] timeout the number of seconds to wait for the counter or
# `nil` to block indefinitely
# @return [Boolean] `true` if the `count` reaches zero else false on
# `timeout` or on `reset` or if the barrier is broken
def wait(timeout = nil)
synchronize do
return false unless @generation.status == :waiting
@number_waiting += 1
if @number_waiting == @parties
@action.call if @action
ns_generation_done @generation, :fulfilled
true
else
generation = @generation
if ns_wait_until(timeout) { generation.status != :waiting }
generation.status == :fulfilled
else
ns_generation_done generation, :broken, false
false
end
end
end
end
# resets the barrier to its initial state
# If there is at least one waiting thread, it will be woken up, the `wait`
# method will return false and the barrier will be broken
# If the barrier is broken, this method restores it to the original state
#
# @return [nil]
def reset
synchronize { ns_generation_done @generation, :reset }
end
# A barrier can be broken when:
# - a thread called the `reset` method while at least one other thread was waiting
# - at least one thread timed out on `wait` method
#
# A broken barrier can be restored using `reset` it's safer to create a new one
# @return [Boolean] true if the barrier is broken otherwise false
def broken?
synchronize { @generation.status != :waiting }
end
protected
def ns_generation_done(generation, status, continue = true)
generation.status = status
ns_next_generation if continue
ns_broadcast
end
def ns_next_generation
@generation = Generation.new(:waiting)
@number_waiting = 0
end
def ns_initialize(parties, &block)
@parties = parties
@action = block
ns_next_generation
end
end
end
@@ -0,0 +1,109 @@
require 'thread'
require 'concurrent/synchronization/lockable_object'
module Concurrent
# Old school kernel-style event reminiscent of Win32 programming in C++.
#
# When an `Event` is created it is in the `unset` state. Threads can choose to
# `#wait` on the event, blocking until released by another thread. When one
# thread wants to alert all blocking threads it calls the `#set` method which
# will then wake up all listeners. Once an `Event` has been set it remains set.
# New threads calling `#wait` will return immediately. An `Event` may be
# `#reset` at any time once it has been set.
#
# @see http://msdn.microsoft.com/en-us/library/windows/desktop/ms682655.aspx
# @example
# event = Concurrent::Event.new
#
# t1 = Thread.new do
# puts "t1 is waiting"
# event.wait(1)
# puts "event occurred"
# end
#
# t2 = Thread.new do
# puts "t2 calling set"
# event.set
# end
#
# [t1, t2].each(&:join)
#
# # prints:
# # t1 is waiting
# # t2 calling set
# # event occurred
class Event < Synchronization::LockableObject
# Creates a new `Event` in the unset state. Threads calling `#wait` on the
# `Event` will block.
def initialize
super
synchronize { ns_initialize }
end
# Is the object in the set state?
#
# @return [Boolean] indicating whether or not the `Event` has been set
def set?
synchronize { @set }
end
# Trigger the event, setting the state to `set` and releasing all threads
# waiting on the event. Has no effect if the `Event` has already been set.
#
# @return [Boolean] should always return `true`
def set
synchronize { ns_set }
end
def try?
synchronize { @set ? false : ns_set }
end
# Reset a previously set event back to the `unset` state.
# Has no effect if the `Event` has not yet been set.
#
# @return [Boolean] should always return `true`
def reset
synchronize do
if @set
@set = false
@iteration +=1
end
true
end
end
# Wait a given number of seconds for the `Event` to be set by another
# thread. Will wait forever when no `timeout` value is given. Returns
# immediately if the `Event` has already been set.
#
# @return [Boolean] true if the `Event` was set before timeout else false
def wait(timeout = nil)
synchronize do
unless @set
iteration = @iteration
ns_wait_until(timeout) { iteration < @iteration || @set }
else
true
end
end
end
protected
def ns_set
unless @set
@set = true
ns_broadcast
end
true
end
def ns_initialize
@set = false
@iteration = 0
end
end
end
@@ -0,0 +1,109 @@
require 'concurrent/constants'
require_relative 'locals'
module Concurrent
# A `FiberLocalVar` is a variable where the value is different for each fiber.
# Each variable may have a default value, but when you modify the variable only
# the current fiber will ever see that change.
#
# This is similar to Ruby's built-in fiber-local variables (`Thread.current[:name]`),
# but with these major advantages:
# * `FiberLocalVar` has its own identity, it doesn't need a Symbol.
# * Each Ruby's built-in fiber-local variable leaks some memory forever (it's a Symbol held forever on the fiber),
# so it's only OK to create a small amount of them.
# `FiberLocalVar` has no such issue and it is fine to create many of them.
# * Ruby's built-in fiber-local variables leak forever the value set on each fiber (unless set to nil explicitly).
# `FiberLocalVar` automatically removes the mapping for each fiber once the `FiberLocalVar` instance is GC'd.
#
# @example
# v = FiberLocalVar.new(14)
# v.value #=> 14
# v.value = 2
# v.value #=> 2
#
# @example
# v = FiberLocalVar.new(14)
#
# Fiber.new do
# v.value #=> 14
# v.value = 1
# v.value #=> 1
# end.resume
#
# Fiber.new do
# v.value #=> 14
# v.value = 2
# v.value #=> 2
# end.resume
#
# v.value #=> 14
class FiberLocalVar
LOCALS = FiberLocals.new
# Creates a fiber local variable.
#
# @param [Object] default the default value when otherwise unset
# @param [Proc] default_block Optional block that gets called to obtain the
# default value for each fiber
def initialize(default = nil, &default_block)
if default && block_given?
raise ArgumentError, "Cannot use both value and block as default value"
end
if block_given?
@default_block = default_block
@default = nil
else
@default_block = nil
@default = default
end
@index = LOCALS.next_index(self)
end
# Returns the value in the current fiber's copy of this fiber-local variable.
#
# @return [Object] the current value
def value
LOCALS.fetch(@index) { default }
end
# Sets the current fiber's copy of this fiber-local variable to the specified value.
#
# @param [Object] value the value to set
# @return [Object] the new value
def value=(value)
LOCALS.set(@index, value)
end
# Bind the given value to fiber local storage during
# execution of the given block.
#
# @param [Object] value the value to bind
# @yield the operation to be performed with the bound variable
# @return [Object] the value
def bind(value)
if block_given?
old_value = self.value
self.value = value
begin
yield
ensure
self.value = old_value
end
end
end
protected
# @!visibility private
def default
if @default_block
self.value = @default_block.call
else
@default
end
end
end
end
@@ -0,0 +1,43 @@
if Concurrent.on_jruby?
require 'concurrent/utility/native_extension_loader'
module Concurrent
# @!macro count_down_latch
# @!visibility private
# @!macro internal_implementation_note
class JavaCountDownLatch
# @!macro count_down_latch_method_initialize
def initialize(count = 1)
Utility::NativeInteger.ensure_integer_and_bounds(count)
Utility::NativeInteger.ensure_positive(count)
@latch = java.util.concurrent.CountDownLatch.new(count)
end
# @!macro count_down_latch_method_wait
def wait(timeout = nil)
result = nil
if timeout.nil?
Synchronization::JRuby.sleep_interruptibly { @latch.await }
result = true
else
Synchronization::JRuby.sleep_interruptibly do
result = @latch.await(1000 * timeout, java.util.concurrent.TimeUnit::MILLISECONDS)
end
end
result
end
# @!macro count_down_latch_method_count_down
def count_down
@latch.countDown
end
# @!macro count_down_latch_method_count
def count
@latch.getCount
end
end
end
end
@@ -0,0 +1,189 @@
require 'fiber'
require 'concurrent/utility/engine'
require 'concurrent/constants'
module Concurrent
# @!visibility private
# @!macro internal_implementation_note
#
# An abstract implementation of local storage, with sub-classes for
# per-thread and per-fiber locals.
#
# Each execution context (EC, thread or fiber) has a lazily initialized array
# of local variable values. Each time a new local variable is created, we
# allocate an "index" for it.
#
# For example, if the allocated index is 1, that means slot #1 in EVERY EC's
# locals array will be used for the value of that variable.
#
# The good thing about using a per-EC structure to hold values, rather than
# a global, is that no synchronization is needed when reading and writing
# those values (since the structure is only ever accessed by a single
# thread).
#
# Of course, when a local variable is GC'd, 1) we need to recover its index
# for use by other new local variables (otherwise the locals arrays could
# get bigger and bigger with time), and 2) we need to null out all the
# references held in the now-unused slots (both to avoid blocking GC of those
# objects, and also to prevent "stale" values from being passed on to a new
# local when the index is reused).
#
# Because we need to null out freed slots, we need to keep references to
# ALL the locals arrays, so we can null out the appropriate slots in all of
# them. This is why we need to use a finalizer to clean up the locals array
# when the EC goes out of scope.
class AbstractLocals
def initialize
@free = []
@lock = Mutex.new
@all_arrays = {}
@next = 0
end
def synchronize
@lock.synchronize { yield }
end
if Concurrent.on_cruby?
def weak_synchronize
yield
end
else
alias_method :weak_synchronize, :synchronize
end
def next_index(local)
index = synchronize do
if @free.empty?
@next += 1
else
@free.pop
end
end
# When the local goes out of scope, we should free the associated index
# and all values stored into it.
ObjectSpace.define_finalizer(local, local_finalizer(index))
index
end
def free_index(index)
weak_synchronize do
# The cost of GC'ing a TLV is linear in the number of ECs using local
# variables. But that is natural! More ECs means more storage is used
# per local variable. So naturally more CPU time is required to free
# more storage.
#
# DO NOT use each_value which might conflict with new pair assignment
# into the hash in #set method.
@all_arrays.values.each do |locals|
locals[index] = nil
end
# free index has to be published after the arrays are cleared:
@free << index
end
end
def fetch(index)
locals = self.locals
value = locals ? locals[index] : nil
if nil == value
yield
elsif NULL.equal?(value)
nil
else
value
end
end
def set(index, value)
locals = self.locals!
locals[index] = (nil == value ? NULL : value)
value
end
private
# When the local goes out of scope, clean up that slot across all locals currently assigned.
def local_finalizer(index)
proc do
free_index(index)
end
end
# When a thread/fiber goes out of scope, remove the array from @all_arrays.
def thread_fiber_finalizer(array_object_id)
proc do
weak_synchronize do
@all_arrays.delete(array_object_id)
end
end
end
# Returns the locals for the current scope, or nil if none exist.
def locals
raise NotImplementedError
end
# Returns the locals for the current scope, creating them if necessary.
def locals!
raise NotImplementedError
end
end
# @!visibility private
# @!macro internal_implementation_note
# An array-backed storage of indexed variables per thread.
class ThreadLocals < AbstractLocals
def locals
Thread.current.thread_variable_get(:concurrent_thread_locals)
end
def locals!
thread = Thread.current
locals = thread.thread_variable_get(:concurrent_thread_locals)
unless locals
locals = thread.thread_variable_set(:concurrent_thread_locals, [])
weak_synchronize do
@all_arrays[locals.object_id] = locals
end
# When the thread goes out of scope, we should delete the associated locals:
ObjectSpace.define_finalizer(thread, thread_fiber_finalizer(locals.object_id))
end
locals
end
end
# @!visibility private
# @!macro internal_implementation_note
# An array-backed storage of indexed variables per fiber.
class FiberLocals < AbstractLocals
def locals
Thread.current[:concurrent_fiber_locals]
end
def locals!
thread = Thread.current
locals = thread[:concurrent_fiber_locals]
unless locals
locals = thread[:concurrent_fiber_locals] = []
weak_synchronize do
@all_arrays[locals.object_id] = locals
end
# When the fiber goes out of scope, we should delete the associated locals:
ObjectSpace.define_finalizer(Fiber.current, thread_fiber_finalizer(locals.object_id))
end
locals
end
end
private_constant :AbstractLocals, :ThreadLocals, :FiberLocals
end
@@ -0,0 +1,28 @@
require 'concurrent/utility/engine'
require_relative 'fiber_local_var'
require_relative 'thread_local_var'
module Concurrent
# @!visibility private
def self.mutex_owned_per_thread?
return false if Concurrent.on_jruby? || Concurrent.on_truffleruby?
mutex = Mutex.new
# Lock the mutex:
mutex.synchronize do
# Check if the mutex is still owned in a child fiber:
Fiber.new { mutex.owned? }.resume
end
end
if mutex_owned_per_thread?
LockLocalVar = ThreadLocalVar
else
LockLocalVar = FiberLocalVar
end
# Either {FiberLocalVar} or {ThreadLocalVar} depending on whether Mutex (and Monitor)
# are held, respectively, per Fiber or per Thread.
class LockLocalVar
end
end
@@ -0,0 +1,68 @@
require 'concurrent/synchronization/safe_initialization'
module Concurrent
# @!macro atomic_boolean
# @!visibility private
# @!macro internal_implementation_note
class MutexAtomicBoolean
extend Concurrent::Synchronization::SafeInitialization
# @!macro atomic_boolean_method_initialize
def initialize(initial = false)
super()
@Lock = ::Mutex.new
@value = !!initial
end
# @!macro atomic_boolean_method_value_get
def value
synchronize { @value }
end
# @!macro atomic_boolean_method_value_set
def value=(value)
synchronize { @value = !!value }
end
# @!macro atomic_boolean_method_true_question
def true?
synchronize { @value }
end
# @!macro atomic_boolean_method_false_question
def false?
synchronize { !@value }
end
# @!macro atomic_boolean_method_make_true
def make_true
synchronize { ns_make_value(true) }
end
# @!macro atomic_boolean_method_make_false
def make_false
synchronize { ns_make_value(false) }
end
protected
# @!visibility private
def synchronize
if @Lock.owned?
yield
else
@Lock.synchronize { yield }
end
end
private
# @!visibility private
def ns_make_value(value)
old = @value
@value = value
old != @value
end
end
end
@@ -0,0 +1,81 @@
require 'concurrent/synchronization/safe_initialization'
require 'concurrent/utility/native_integer'
module Concurrent
# @!macro atomic_fixnum
# @!visibility private
# @!macro internal_implementation_note
class MutexAtomicFixnum
extend Concurrent::Synchronization::SafeInitialization
# @!macro atomic_fixnum_method_initialize
def initialize(initial = 0)
super()
@Lock = ::Mutex.new
ns_set(initial)
end
# @!macro atomic_fixnum_method_value_get
def value
synchronize { @value }
end
# @!macro atomic_fixnum_method_value_set
def value=(value)
synchronize { ns_set(value) }
end
# @!macro atomic_fixnum_method_increment
def increment(delta = 1)
synchronize { ns_set(@value + delta.to_i) }
end
alias_method :up, :increment
# @!macro atomic_fixnum_method_decrement
def decrement(delta = 1)
synchronize { ns_set(@value - delta.to_i) }
end
alias_method :down, :decrement
# @!macro atomic_fixnum_method_compare_and_set
def compare_and_set(expect, update)
synchronize do
if @value == expect.to_i
@value = update.to_i
true
else
false
end
end
end
# @!macro atomic_fixnum_method_update
def update
synchronize do
@value = yield @value
end
end
protected
# @!visibility private
def synchronize
if @Lock.owned?
yield
else
@Lock.synchronize { yield }
end
end
private
# @!visibility private
def ns_set(value)
Utility::NativeInteger.ensure_integer_and_bounds value
@value = value
end
end
end
@@ -0,0 +1,44 @@
require 'concurrent/synchronization/lockable_object'
require 'concurrent/utility/native_integer'
module Concurrent
# @!macro count_down_latch
# @!visibility private
# @!macro internal_implementation_note
class MutexCountDownLatch < Synchronization::LockableObject
# @!macro count_down_latch_method_initialize
def initialize(count = 1)
Utility::NativeInteger.ensure_integer_and_bounds count
Utility::NativeInteger.ensure_positive count
super()
synchronize { ns_initialize count }
end
# @!macro count_down_latch_method_wait
def wait(timeout = nil)
synchronize { ns_wait_until(timeout) { @count == 0 } }
end
# @!macro count_down_latch_method_count_down
def count_down
synchronize do
@count -= 1 if @count > 0
ns_broadcast if @count == 0
end
end
# @!macro count_down_latch_method_count
def count
synchronize { @count }
end
protected
def ns_initialize(count)
@count = count
end
end
end
@@ -0,0 +1,131 @@
require 'concurrent/synchronization/lockable_object'
require 'concurrent/utility/native_integer'
module Concurrent
# @!macro semaphore
# @!visibility private
# @!macro internal_implementation_note
class MutexSemaphore < Synchronization::LockableObject
# @!macro semaphore_method_initialize
def initialize(count)
Utility::NativeInteger.ensure_integer_and_bounds count
super()
synchronize { ns_initialize count }
end
# @!macro semaphore_method_acquire
def acquire(permits = 1)
Utility::NativeInteger.ensure_integer_and_bounds permits
Utility::NativeInteger.ensure_positive permits
synchronize do
try_acquire_timed(permits, nil)
end
return unless block_given?
begin
yield
ensure
release(permits)
end
end
# @!macro semaphore_method_available_permits
def available_permits
synchronize { @free }
end
# @!macro semaphore_method_drain_permits
#
# Acquires and returns all permits that are immediately available.
#
# @return [Integer]
def drain_permits
synchronize do
@free.tap { |_| @free = 0 }
end
end
# @!macro semaphore_method_try_acquire
def try_acquire(permits = 1, timeout = nil)
Utility::NativeInteger.ensure_integer_and_bounds permits
Utility::NativeInteger.ensure_positive permits
acquired = synchronize do
if timeout.nil?
try_acquire_now(permits)
else
try_acquire_timed(permits, timeout)
end
end
return acquired unless block_given?
return unless acquired
begin
yield
ensure
release(permits)
end
end
# @!macro semaphore_method_release
def release(permits = 1)
Utility::NativeInteger.ensure_integer_and_bounds permits
Utility::NativeInteger.ensure_positive permits
synchronize do
@free += permits
permits.times { ns_signal }
end
nil
end
# Shrinks the number of available permits by the indicated reduction.
#
# @param [Fixnum] reduction Number of permits to remove.
#
# @raise [ArgumentError] if `reduction` is not an integer or is negative
#
# @raise [ArgumentError] if `@free` - `@reduction` is less than zero
#
# @return [nil]
#
# @!visibility private
def reduce_permits(reduction)
Utility::NativeInteger.ensure_integer_and_bounds reduction
Utility::NativeInteger.ensure_positive reduction
synchronize { @free -= reduction }
nil
end
protected
# @!visibility private
def ns_initialize(count)
@free = count
end
private
# @!visibility private
def try_acquire_now(permits)
if @free >= permits
@free -= permits
true
else
false
end
end
# @!visibility private
def try_acquire_timed(permits, timeout)
ns_wait_until(timeout) { try_acquire_now(permits) }
end
end
end
@@ -0,0 +1,255 @@
require 'thread'
require 'concurrent/atomic/atomic_fixnum'
require 'concurrent/errors'
require 'concurrent/synchronization/object'
require 'concurrent/synchronization/lock'
module Concurrent
# Ruby read-write lock implementation
#
# Allows any number of concurrent readers, but only one concurrent writer
# (And if the "write" lock is taken, any readers who come along will have to wait)
#
# If readers are already active when a writer comes along, the writer will wait for
# all the readers to finish before going ahead.
# Any additional readers that come when the writer is already waiting, will also
# wait (so writers are not starved).
#
# This implementation is based on `java.util.concurrent.ReentrantReadWriteLock`.
#
# @example
# lock = Concurrent::ReadWriteLock.new
# lock.with_read_lock { data.retrieve }
# lock.with_write_lock { data.modify! }
#
# @note Do **not** try to acquire the write lock while already holding a read lock
# **or** try to acquire the write lock while you already have it.
# This will lead to deadlock
#
# @see http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/locks/ReentrantReadWriteLock.html java.util.concurrent.ReentrantReadWriteLock
class ReadWriteLock < Synchronization::Object
# @!visibility private
WAITING_WRITER = 1 << 15
# @!visibility private
RUNNING_WRITER = 1 << 29
# @!visibility private
MAX_READERS = WAITING_WRITER - 1
# @!visibility private
MAX_WRITERS = RUNNING_WRITER - MAX_READERS - 1
safe_initialization!
# Implementation notes:
# A goal is to make the uncontended path for both readers/writers lock-free
# Only if there is reader-writer or writer-writer contention, should locks be used
# Internal state is represented by a single integer ("counter"), and updated
# using atomic compare-and-swap operations
# When the counter is 0, the lock is free
# Each reader increments the counter by 1 when acquiring a read lock
# (and decrements by 1 when releasing the read lock)
# The counter is increased by (1 << 15) for each writer waiting to acquire the
# write lock, and by (1 << 29) if the write lock is taken
# Create a new `ReadWriteLock` in the unlocked state.
def initialize
super()
@Counter = AtomicFixnum.new(0) # single integer which represents lock state
@ReadLock = Synchronization::Lock.new
@WriteLock = Synchronization::Lock.new
end
# Execute a block operation within a read lock.
#
# @yield the task to be performed within the lock.
#
# @return [Object] the result of the block operation.
#
# @raise [ArgumentError] when no block is given.
# @raise [Concurrent::ResourceLimitError] if the maximum number of readers
# is exceeded.
def with_read_lock
raise ArgumentError.new('no block given') unless block_given?
acquire_read_lock
begin
yield
ensure
release_read_lock
end
end
# Execute a block operation within a write lock.
#
# @yield the task to be performed within the lock.
#
# @return [Object] the result of the block operation.
#
# @raise [ArgumentError] when no block is given.
# @raise [Concurrent::ResourceLimitError] if the maximum number of readers
# is exceeded.
def with_write_lock
raise ArgumentError.new('no block given') unless block_given?
acquire_write_lock
begin
yield
ensure
release_write_lock
end
end
# Acquire a read lock. If a write lock has been acquired will block until
# it is released. Will not block if other read locks have been acquired.
#
# @return [Boolean] true if the lock is successfully acquired
#
# @raise [Concurrent::ResourceLimitError] if the maximum number of readers
# is exceeded.
def acquire_read_lock
while true
c = @Counter.value
raise ResourceLimitError.new('Too many reader threads') if max_readers?(c)
# If a writer is waiting when we first queue up, we need to wait
if waiting_writer?(c)
@ReadLock.wait_until { !waiting_writer? }
# after a reader has waited once, they are allowed to "barge" ahead of waiting writers
# but if a writer is *running*, the reader still needs to wait (naturally)
while true
c = @Counter.value
if running_writer?(c)
@ReadLock.wait_until { !running_writer? }
else
return if @Counter.compare_and_set(c, c+1)
end
end
else
break if @Counter.compare_and_set(c, c+1)
end
end
true
end
# Release a previously acquired read lock.
#
# @return [Boolean] true if the lock is successfully released
def release_read_lock
while true
c = @Counter.value
if @Counter.compare_and_set(c, c-1)
# If one or more writers were waiting, and we were the last reader, wake a writer up
if waiting_writer?(c) && running_readers(c) == 1
@WriteLock.signal
end
break
end
end
true
end
# Acquire a write lock. Will block and wait for all active readers and writers.
#
# @return [Boolean] true if the lock is successfully acquired
#
# @raise [Concurrent::ResourceLimitError] if the maximum number of writers
# is exceeded.
def acquire_write_lock
while true
c = @Counter.value
raise ResourceLimitError.new('Too many writer threads') if max_writers?(c)
if c == 0 # no readers OR writers running
# if we successfully swap the RUNNING_WRITER bit on, then we can go ahead
break if @Counter.compare_and_set(0, RUNNING_WRITER)
elsif @Counter.compare_and_set(c, c+WAITING_WRITER)
while true
# Now we have successfully incremented, so no more readers will be able to increment
# (they will wait instead)
# However, readers OR writers could decrement right here, OR another writer could increment
@WriteLock.wait_until do
# So we have to do another check inside the synchronized section
# If a writer OR reader is running, then go to sleep
c = @Counter.value
!running_writer?(c) && !running_readers?(c)
end
# We just came out of a wait
# If we successfully turn the RUNNING_WRITER bit on with an atomic swap,
# Then we are OK to stop waiting and go ahead
# Otherwise go back and wait again
c = @Counter.value
break if !running_writer?(c) && !running_readers?(c) && @Counter.compare_and_set(c, c+RUNNING_WRITER-WAITING_WRITER)
end
break
end
end
true
end
# Release a previously acquired write lock.
#
# @return [Boolean] true if the lock is successfully released
def release_write_lock
return true unless running_writer?
c = @Counter.update { |counter| counter - RUNNING_WRITER }
@ReadLock.broadcast
@WriteLock.signal if waiting_writers(c) > 0
true
end
# Queries if the write lock is held by any thread.
#
# @return [Boolean] true if the write lock is held else false`
def write_locked?
@Counter.value >= RUNNING_WRITER
end
# Queries whether any threads are waiting to acquire the read or write lock.
#
# @return [Boolean] true if any threads are waiting for a lock else false
def has_waiters?
waiting_writer?(@Counter.value)
end
private
# @!visibility private
def running_readers(c = @Counter.value)
c & MAX_READERS
end
# @!visibility private
def running_readers?(c = @Counter.value)
(c & MAX_READERS) > 0
end
# @!visibility private
def running_writer?(c = @Counter.value)
c >= RUNNING_WRITER
end
# @!visibility private
def waiting_writers(c = @Counter.value)
(c & MAX_WRITERS) / WAITING_WRITER
end
# @!visibility private
def waiting_writer?(c = @Counter.value)
c >= WAITING_WRITER
end
# @!visibility private
def max_readers?(c = @Counter.value)
(c & MAX_READERS) == MAX_READERS
end
# @!visibility private
def max_writers?(c = @Counter.value)
(c & MAX_WRITERS) == MAX_WRITERS
end
end
end
@@ -0,0 +1,379 @@
require 'thread'
require 'concurrent/atomic/atomic_reference'
require 'concurrent/atomic/atomic_fixnum'
require 'concurrent/errors'
require 'concurrent/synchronization/object'
require 'concurrent/synchronization/lock'
require 'concurrent/atomic/lock_local_var'
module Concurrent
# Re-entrant read-write lock implementation
#
# Allows any number of concurrent readers, but only one concurrent writer
# (And while the "write" lock is taken, no read locks can be obtained either.
# Hence, the write lock can also be called an "exclusive" lock.)
#
# If another thread has taken a read lock, any thread which wants a write lock
# will block until all the readers release their locks. However, once a thread
# starts waiting to obtain a write lock, any additional readers that come along
# will also wait (so writers are not starved).
#
# A thread can acquire both a read and write lock at the same time. A thread can
# also acquire a read lock OR a write lock more than once. Only when the read (or
# write) lock is released as many times as it was acquired, will the thread
# actually let it go, allowing other threads which might have been waiting
# to proceed. Therefore the lock can be upgraded by first acquiring
# read lock and then write lock and that the lock can be downgraded by first
# having both read and write lock a releasing just the write lock.
#
# If both read and write locks are acquired by the same thread, it is not strictly
# necessary to release them in the same order they were acquired. In other words,
# the following code is legal:
#
# @example
# lock = Concurrent::ReentrantReadWriteLock.new
# lock.acquire_write_lock
# lock.acquire_read_lock
# lock.release_write_lock
# # At this point, the current thread is holding only a read lock, not a write
# # lock. So other threads can take read locks, but not a write lock.
# lock.release_read_lock
# # Now the current thread is not holding either a read or write lock, so
# # another thread could potentially acquire a write lock.
#
# This implementation was inspired by `java.util.concurrent.ReentrantReadWriteLock`.
#
# @example
# lock = Concurrent::ReentrantReadWriteLock.new
# lock.with_read_lock { data.retrieve }
# lock.with_write_lock { data.modify! }
#
# @see http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/locks/ReentrantReadWriteLock.html java.util.concurrent.ReentrantReadWriteLock
class ReentrantReadWriteLock < Synchronization::Object
# Implementation notes:
#
# A goal is to make the uncontended path for both readers/writers mutex-free
# Only if there is reader-writer or writer-writer contention, should mutexes be used
# Otherwise, a single CAS operation is all we need to acquire/release a lock
#
# Internal state is represented by a single integer ("counter"), and updated
# using atomic compare-and-swap operations
# When the counter is 0, the lock is free
# Each thread which has one OR MORE read locks increments the counter by 1
# (and decrements by 1 when releasing the read lock)
# The counter is increased by (1 << 15) for each writer waiting to acquire the
# write lock, and by (1 << 29) if the write lock is taken
#
# Additionally, each thread uses a thread-local variable to count how many times
# it has acquired a read lock, AND how many times it has acquired a write lock.
# It uses a similar trick; an increment of 1 means a read lock was taken, and
# an increment of (1 << 15) means a write lock was taken
# This is what makes re-entrancy possible
#
# 2 rules are followed to ensure good liveness properties:
# 1) Once a writer has queued up and is waiting for a write lock, no other thread
# can take a lock without waiting
# 2) When a write lock is released, readers are given the "first chance" to wake
# up and acquire a read lock
# Following these rules means readers and writers tend to "take turns", so neither
# can starve the other, even under heavy contention
# @!visibility private
READER_BITS = 15
# @!visibility private
WRITER_BITS = 14
# Used with @Counter:
# @!visibility private
WAITING_WRITER = 1 << READER_BITS
# @!visibility private
RUNNING_WRITER = 1 << (READER_BITS + WRITER_BITS)
# @!visibility private
MAX_READERS = WAITING_WRITER - 1
# @!visibility private
MAX_WRITERS = RUNNING_WRITER - MAX_READERS - 1
# Used with @HeldCount:
# @!visibility private
WRITE_LOCK_HELD = 1 << READER_BITS
# @!visibility private
READ_LOCK_MASK = WRITE_LOCK_HELD - 1
# @!visibility private
WRITE_LOCK_MASK = MAX_WRITERS
safe_initialization!
# Create a new `ReentrantReadWriteLock` in the unlocked state.
def initialize
super()
@Counter = AtomicFixnum.new(0) # single integer which represents lock state
@ReadQueue = Synchronization::Lock.new # used to queue waiting readers
@WriteQueue = Synchronization::Lock.new # used to queue waiting writers
@HeldCount = LockLocalVar.new(0) # indicates # of R & W locks held by this thread
end
# Execute a block operation within a read lock.
#
# @yield the task to be performed within the lock.
#
# @return [Object] the result of the block operation.
#
# @raise [ArgumentError] when no block is given.
# @raise [Concurrent::ResourceLimitError] if the maximum number of readers
# is exceeded.
def with_read_lock
raise ArgumentError.new('no block given') unless block_given?
acquire_read_lock
begin
yield
ensure
release_read_lock
end
end
# Execute a block operation within a write lock.
#
# @yield the task to be performed within the lock.
#
# @return [Object] the result of the block operation.
#
# @raise [ArgumentError] when no block is given.
# @raise [Concurrent::ResourceLimitError] if the maximum number of readers
# is exceeded.
def with_write_lock
raise ArgumentError.new('no block given') unless block_given?
acquire_write_lock
begin
yield
ensure
release_write_lock
end
end
# Acquire a read lock. If a write lock is held by another thread, will block
# until it is released.
#
# @return [Boolean] true if the lock is successfully acquired
#
# @raise [Concurrent::ResourceLimitError] if the maximum number of readers
# is exceeded.
def acquire_read_lock
if (held = @HeldCount.value) > 0
# If we already have a lock, there's no need to wait
if held & READ_LOCK_MASK == 0
# But we do need to update the counter, if we were holding a write
# lock but not a read lock
@Counter.update { |c| c + 1 }
end
@HeldCount.value = held + 1
return true
end
while true
c = @Counter.value
raise ResourceLimitError.new('Too many reader threads') if max_readers?(c)
# If a writer is waiting OR running when we first queue up, we need to wait
if waiting_or_running_writer?(c)
# Before going to sleep, check again with the ReadQueue mutex held
@ReadQueue.synchronize do
@ReadQueue.ns_wait if waiting_or_running_writer?
end
# Note: the above 'synchronize' block could have used #wait_until,
# but that waits repeatedly in a loop, checking the wait condition
# each time it wakes up (to protect against spurious wakeups)
# But we are already in a loop, which is only broken when we successfully
# acquire the lock! So we don't care about spurious wakeups, and would
# rather not pay the extra overhead of using #wait_until
# After a reader has waited once, they are allowed to "barge" ahead of waiting writers
# But if a writer is *running*, the reader still needs to wait (naturally)
while true
c = @Counter.value
if running_writer?(c)
@ReadQueue.synchronize do
@ReadQueue.ns_wait if running_writer?
end
elsif @Counter.compare_and_set(c, c+1)
@HeldCount.value = held + 1
return true
end
end
elsif @Counter.compare_and_set(c, c+1)
@HeldCount.value = held + 1
return true
end
end
end
# Try to acquire a read lock and return true if we succeed. If it cannot be
# acquired immediately, return false.
#
# @return [Boolean] true if the lock is successfully acquired
def try_read_lock
if (held = @HeldCount.value) > 0
if held & READ_LOCK_MASK == 0
# If we hold a write lock, but not a read lock...
@Counter.update { |c| c + 1 }
end
@HeldCount.value = held + 1
return true
else
c = @Counter.value
if !waiting_or_running_writer?(c) && @Counter.compare_and_set(c, c+1)
@HeldCount.value = held + 1
return true
end
end
false
end
# Release a previously acquired read lock.
#
# @return [Boolean] true if the lock is successfully released
def release_read_lock
held = @HeldCount.value = @HeldCount.value - 1
rlocks_held = held & READ_LOCK_MASK
if rlocks_held == 0
c = @Counter.update { |counter| counter - 1 }
# If one or more writers were waiting, and we were the last reader, wake a writer up
if waiting_or_running_writer?(c) && running_readers(c) == 0
@WriteQueue.signal
end
elsif rlocks_held == READ_LOCK_MASK
raise IllegalOperationError, "Cannot release a read lock which is not held"
end
true
end
# Acquire a write lock. Will block and wait for all active readers and writers.
#
# @return [Boolean] true if the lock is successfully acquired
#
# @raise [Concurrent::ResourceLimitError] if the maximum number of writers
# is exceeded.
def acquire_write_lock
if (held = @HeldCount.value) >= WRITE_LOCK_HELD
# if we already have a write (exclusive) lock, there's no need to wait
@HeldCount.value = held + WRITE_LOCK_HELD
return true
end
while true
c = @Counter.value
raise ResourceLimitError.new('Too many writer threads') if max_writers?(c)
# To go ahead and take the lock without waiting, there must be no writer
# running right now, AND no writers who came before us still waiting to
# acquire the lock
# Additionally, if any read locks have been taken, we must hold all of them
if held > 0 && @Counter.compare_and_set(1, c+RUNNING_WRITER)
# If we are the only one reader and successfully swap the RUNNING_WRITER bit on, then we can go ahead
@HeldCount.value = held + WRITE_LOCK_HELD
return true
elsif @Counter.compare_and_set(c, c+WAITING_WRITER)
while true
# Now we have successfully incremented, so no more readers will be able to increment
# (they will wait instead)
# However, readers OR writers could decrement right here
@WriteQueue.synchronize do
# So we have to do another check inside the synchronized section
# If a writer OR another reader is running, then go to sleep
c = @Counter.value
@WriteQueue.ns_wait if running_writer?(c) || running_readers(c) != held
end
# Note: if you are thinking of replacing the above 'synchronize' block
# with #wait_until, read the comment in #acquire_read_lock first!
# We just came out of a wait
# If we successfully turn the RUNNING_WRITER bit on with an atomic swap,
# then we are OK to stop waiting and go ahead
# Otherwise go back and wait again
c = @Counter.value
if !running_writer?(c) &&
running_readers(c) == held &&
@Counter.compare_and_set(c, c+RUNNING_WRITER-WAITING_WRITER)
@HeldCount.value = held + WRITE_LOCK_HELD
return true
end
end
end
end
end
# Try to acquire a write lock and return true if we succeed. If it cannot be
# acquired immediately, return false.
#
# @return [Boolean] true if the lock is successfully acquired
def try_write_lock
if (held = @HeldCount.value) >= WRITE_LOCK_HELD
@HeldCount.value = held + WRITE_LOCK_HELD
return true
else
c = @Counter.value
if !waiting_or_running_writer?(c) &&
running_readers(c) == held &&
@Counter.compare_and_set(c, c+RUNNING_WRITER)
@HeldCount.value = held + WRITE_LOCK_HELD
return true
end
end
false
end
# Release a previously acquired write lock.
#
# @return [Boolean] true if the lock is successfully released
def release_write_lock
held = @HeldCount.value = @HeldCount.value - WRITE_LOCK_HELD
wlocks_held = held & WRITE_LOCK_MASK
if wlocks_held == 0
c = @Counter.update { |counter| counter - RUNNING_WRITER }
@ReadQueue.broadcast
@WriteQueue.signal if waiting_writers(c) > 0
elsif wlocks_held == WRITE_LOCK_MASK
raise IllegalOperationError, "Cannot release a write lock which is not held"
end
true
end
private
# @!visibility private
def running_readers(c = @Counter.value)
c & MAX_READERS
end
# @!visibility private
def running_readers?(c = @Counter.value)
(c & MAX_READERS) > 0
end
# @!visibility private
def running_writer?(c = @Counter.value)
c >= RUNNING_WRITER
end
# @!visibility private
def waiting_writers(c = @Counter.value)
(c & MAX_WRITERS) >> READER_BITS
end
# @!visibility private
def waiting_or_running_writer?(c = @Counter.value)
c >= WAITING_WRITER
end
# @!visibility private
def max_readers?(c = @Counter.value)
(c & MAX_READERS) == MAX_READERS
end
# @!visibility private
def max_writers?(c = @Counter.value)
(c & MAX_WRITERS) == MAX_WRITERS
end
end
end
@@ -0,0 +1,163 @@
require 'concurrent/atomic/mutex_semaphore'
module Concurrent
###################################################################
# @!macro semaphore_method_initialize
#
# Create a new `Semaphore` with the initial `count`.
#
# @param [Fixnum] count the initial count
#
# @raise [ArgumentError] if `count` is not an integer
# @!macro semaphore_method_acquire
#
# Acquires the given number of permits from this semaphore,
# blocking until all are available. If a block is given,
# yields to it and releases the permits afterwards.
#
# @param [Fixnum] permits Number of permits to acquire
#
# @raise [ArgumentError] if `permits` is not an integer or is less than zero
#
# @return [nil, BasicObject] Without a block, `nil` is returned. If a block
# is given, its return value is returned.
# @!macro semaphore_method_available_permits
#
# Returns the current number of permits available in this semaphore.
#
# @return [Integer]
# @!macro semaphore_method_drain_permits
#
# Acquires and returns all permits that are immediately available.
#
# @return [Integer]
# @!macro semaphore_method_try_acquire
#
# Acquires the given number of permits from this semaphore,
# only if all are available at the time of invocation or within
# `timeout` interval. If a block is given, yields to it if the permits
# were successfully acquired, and releases them afterward, returning the
# block's return value.
#
# @param [Fixnum] permits the number of permits to acquire
#
# @param [Fixnum] timeout the number of seconds to wait for the counter
# or `nil` to return immediately
#
# @raise [ArgumentError] if `permits` is not an integer or is less than zero
#
# @return [true, false, nil, BasicObject] `false` if no permits are
# available, `true` when acquired a permit. If a block is given, the
# block's return value is returned if the permits were acquired; if not,
# `nil` is returned.
# @!macro semaphore_method_release
#
# Releases the given number of permits, returning them to the semaphore.
#
# @param [Fixnum] permits Number of permits to return to the semaphore.
#
# @raise [ArgumentError] if `permits` is not a number or is less than zero
#
# @return [nil]
###################################################################
# @!macro semaphore_public_api
#
# @!method initialize(count)
# @!macro semaphore_method_initialize
#
# @!method acquire(permits = 1)
# @!macro semaphore_method_acquire
#
# @!method available_permits
# @!macro semaphore_method_available_permits
#
# @!method drain_permits
# @!macro semaphore_method_drain_permits
#
# @!method try_acquire(permits = 1, timeout = nil)
# @!macro semaphore_method_try_acquire
#
# @!method release(permits = 1)
# @!macro semaphore_method_release
###################################################################
# @!visibility private
# @!macro internal_implementation_note
SemaphoreImplementation = if Concurrent.on_jruby?
require 'concurrent/utility/native_extension_loader'
JavaSemaphore
else
MutexSemaphore
end
private_constant :SemaphoreImplementation
# @!macro semaphore
#
# A counting semaphore. Conceptually, a semaphore maintains a set of
# permits. Each {#acquire} blocks if necessary until a permit is
# available, and then takes it. Each {#release} adds a permit, potentially
# releasing a blocking acquirer.
# However, no actual permit objects are used; the Semaphore just keeps a
# count of the number available and acts accordingly.
# Alternatively, permits may be acquired within a block, and automatically
# released after the block finishes executing.
#
# @!macro semaphore_public_api
# @example
# semaphore = Concurrent::Semaphore.new(2)
#
# t1 = Thread.new do
# semaphore.acquire
# puts "Thread 1 acquired semaphore"
# end
#
# t2 = Thread.new do
# semaphore.acquire
# puts "Thread 2 acquired semaphore"
# end
#
# t3 = Thread.new do
# semaphore.acquire
# puts "Thread 3 acquired semaphore"
# end
#
# t4 = Thread.new do
# sleep(2)
# puts "Thread 4 releasing semaphore"
# semaphore.release
# end
#
# [t1, t2, t3, t4].each(&:join)
#
# # prints:
# # Thread 3 acquired semaphore
# # Thread 2 acquired semaphore
# # Thread 4 releasing semaphore
# # Thread 1 acquired semaphore
#
# @example
# semaphore = Concurrent::Semaphore.new(1)
#
# puts semaphore.available_permits
# semaphore.acquire do
# puts semaphore.available_permits
# end
# puts semaphore.available_permits
#
# # prints:
# # 1
# # 0
# # 1
class Semaphore < SemaphoreImplementation
end
end
@@ -0,0 +1,111 @@
require 'concurrent/constants'
require_relative 'locals'
module Concurrent
# A `ThreadLocalVar` is a variable where the value is different for each thread.
# Each variable may have a default value, but when you modify the variable only
# the current thread will ever see that change.
#
# This is similar to Ruby's built-in thread-local variables (`Thread#thread_variable_get`),
# but with these major advantages:
# * `ThreadLocalVar` has its own identity, it doesn't need a Symbol.
# * Each Ruby's built-in thread-local variable leaks some memory forever (it's a Symbol held forever on the thread),
# so it's only OK to create a small amount of them.
# `ThreadLocalVar` has no such issue and it is fine to create many of them.
# * Ruby's built-in thread-local variables leak forever the value set on each thread (unless set to nil explicitly).
# `ThreadLocalVar` automatically removes the mapping for each thread once the `ThreadLocalVar` instance is GC'd.
#
# @!macro thread_safe_variable_comparison
#
# @example
# v = ThreadLocalVar.new(14)
# v.value #=> 14
# v.value = 2
# v.value #=> 2
#
# @example
# v = ThreadLocalVar.new(14)
#
# t1 = Thread.new do
# v.value #=> 14
# v.value = 1
# v.value #=> 1
# end
#
# t2 = Thread.new do
# v.value #=> 14
# v.value = 2
# v.value #=> 2
# end
#
# v.value #=> 14
class ThreadLocalVar
LOCALS = ThreadLocals.new
# Creates a thread local variable.
#
# @param [Object] default the default value when otherwise unset
# @param [Proc] default_block Optional block that gets called to obtain the
# default value for each thread
def initialize(default = nil, &default_block)
if default && block_given?
raise ArgumentError, "Cannot use both value and block as default value"
end
if block_given?
@default_block = default_block
@default = nil
else
@default_block = nil
@default = default
end
@index = LOCALS.next_index(self)
end
# Returns the value in the current thread's copy of this thread-local variable.
#
# @return [Object] the current value
def value
LOCALS.fetch(@index) { default }
end
# Sets the current thread's copy of this thread-local variable to the specified value.
#
# @param [Object] value the value to set
# @return [Object] the new value
def value=(value)
LOCALS.set(@index, value)
end
# Bind the given value to thread local storage during
# execution of the given block.
#
# @param [Object] value the value to bind
# @yield the operation to be performed with the bound variable
# @return [Object] the value
def bind(value)
if block_given?
old_value = self.value
self.value = value
begin
yield
ensure
self.value = old_value
end
end
end
protected
# @!visibility private
def default
if @default_block
self.value = @default_block.call
else
@default
end
end
end
end