concurrent-ruby 1.1.6 → 1.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +40 -0
  3. data/Gemfile +3 -8
  4. data/{LICENSE.md → LICENSE.txt} +18 -20
  5. data/README.md +43 -23
  6. data/Rakefile +32 -35
  7. data/ext/concurrent-ruby/com/concurrent_ruby/ext/JavaAtomicFixnumLibrary.java +0 -0
  8. data/ext/concurrent-ruby/com/concurrent_ruby/ext/JavaSemaphoreLibrary.java +52 -22
  9. data/lib/concurrent-ruby/concurrent/array.rb +1 -1
  10. data/lib/concurrent-ruby/concurrent/async.rb +10 -20
  11. data/lib/concurrent-ruby/concurrent/atomic/atomic_reference.rb +1 -0
  12. data/lib/concurrent-ruby/concurrent/atomic/event.rb +2 -2
  13. data/lib/concurrent-ruby/concurrent/atomic/mutex_semaphore.rb +18 -2
  14. data/lib/concurrent-ruby/concurrent/atomic/reentrant_read_write_lock.rb +4 -6
  15. data/lib/concurrent-ruby/concurrent/atomic/ruby_thread_local_var.rb +52 -42
  16. data/lib/concurrent-ruby/concurrent/atomic/semaphore.rb +26 -5
  17. data/lib/concurrent-ruby/concurrent/collection/map/mri_map_backend.rb +1 -1
  18. data/lib/concurrent-ruby/concurrent/collection/map/truffleruby_map_backend.rb +14 -0
  19. data/lib/concurrent-ruby/concurrent/collection/ruby_non_concurrent_priority_queue.rb +11 -1
  20. data/lib/concurrent-ruby/concurrent/concurrent_ruby.jar +0 -0
  21. data/lib/concurrent-ruby/concurrent/executor/abstract_executor_service.rb +16 -13
  22. data/lib/concurrent-ruby/concurrent/executor/fixed_thread_pool.rb +20 -3
  23. data/lib/concurrent-ruby/concurrent/executor/java_executor_service.rb +1 -1
  24. data/lib/concurrent-ruby/concurrent/executor/java_thread_pool_executor.rb +17 -1
  25. data/lib/concurrent-ruby/concurrent/executor/ruby_executor_service.rb +10 -4
  26. data/lib/concurrent-ruby/concurrent/executor/ruby_thread_pool_executor.rb +37 -38
  27. data/lib/concurrent-ruby/concurrent/executor/safe_task_executor.rb +5 -5
  28. data/lib/concurrent-ruby/concurrent/executor/thread_pool_executor.rb +2 -1
  29. data/lib/concurrent-ruby/concurrent/hash.rb +1 -1
  30. data/lib/concurrent-ruby/concurrent/immutable_struct.rb +1 -1
  31. data/lib/concurrent-ruby/concurrent/map.rb +13 -4
  32. data/lib/concurrent-ruby/concurrent/mutable_struct.rb +2 -2
  33. data/lib/concurrent-ruby/concurrent/promise.rb +1 -0
  34. data/lib/concurrent-ruby/concurrent/scheduled_task.rb +29 -16
  35. data/lib/concurrent-ruby/concurrent/set.rb +14 -6
  36. data/lib/concurrent-ruby/concurrent/settable_struct.rb +1 -1
  37. data/lib/concurrent-ruby/concurrent/synchronization/lockable_object.rb +3 -5
  38. data/lib/concurrent-ruby/concurrent/synchronization/mutex_lockable_object.rb +12 -0
  39. data/lib/concurrent-ruby/concurrent/synchronization/rbx_lockable_object.rb +6 -0
  40. data/lib/concurrent-ruby/concurrent/thread_safe/util/data_structures.rb +26 -1
  41. data/lib/concurrent-ruby/concurrent/thread_safe/util/striped64.rb +1 -1
  42. data/lib/concurrent-ruby/concurrent/timer_task.rb +11 -34
  43. data/lib/concurrent-ruby/concurrent/tvar.rb +19 -56
  44. data/lib/concurrent-ruby/concurrent/utility/monotonic_time.rb +67 -35
  45. data/lib/concurrent-ruby/concurrent/utility/processor_counter.rb +2 -35
  46. data/lib/concurrent-ruby/concurrent/version.rb +1 -1
  47. data/lib/concurrent-ruby/concurrent-ruby.rb +5 -1
  48. metadata +10 -10
@@ -23,7 +23,14 @@ module Concurrent
23
23
 
24
24
  synchronize do
25
25
  try_acquire_timed(permits, nil)
26
- nil
26
+ end
27
+
28
+ return unless block_given?
29
+
30
+ begin
31
+ yield
32
+ ensure
33
+ release(permits)
27
34
  end
28
35
  end
29
36
 
@@ -48,13 +55,22 @@ module Concurrent
48
55
  Utility::NativeInteger.ensure_integer_and_bounds permits
49
56
  Utility::NativeInteger.ensure_positive permits
50
57
 
51
- synchronize do
58
+ acquired = synchronize do
52
59
  if timeout.nil?
53
60
  try_acquire_now(permits)
54
61
  else
55
62
  try_acquire_timed(permits, timeout)
56
63
  end
57
64
  end
65
+
66
+ return acquired unless block_given?
67
+ return unless acquired
68
+
69
+ begin
70
+ yield
71
+ ensure
72
+ release(permits)
73
+ end
58
74
  end
59
75
 
60
76
  # @!macro semaphore_method_release
@@ -267,12 +267,10 @@ module Concurrent
267
267
  # running right now, AND no writers who came before us still waiting to
268
268
  # acquire the lock
269
269
  # Additionally, if any read locks have been taken, we must hold all of them
270
- if c == held
271
- # If we successfully swap the RUNNING_WRITER bit on, then we can go ahead
272
- if @Counter.compare_and_set(c, c+RUNNING_WRITER)
273
- @HeldCount.value = held + WRITE_LOCK_HELD
274
- return true
275
- end
270
+ if held > 0 && @Counter.compare_and_set(1, c+RUNNING_WRITER)
271
+ # If we are the only one reader and successfully swap the RUNNING_WRITER bit on, then we can go ahead
272
+ @HeldCount.value = held + WRITE_LOCK_HELD
273
+ return true
276
274
  elsif @Counter.compare_and_set(c, c+WAITING_WRITER)
277
275
  while true
278
276
  # Now we have successfully incremented, so no more readers will be able to increment
@@ -28,38 +28,27 @@ module Concurrent
28
28
  # But when a Thread is GC'd, we need to drop the reference to its thread-local
29
29
  # array, so we don't leak memory
30
30
 
31
- # @!visibility private
32
- FREE = []
33
- LOCK = Mutex.new
34
- ARRAYS = {} # used as a hash set
35
- # noinspection RubyClassVariableUsageInspection
36
- @@next = 0
37
- QUEUE = Queue.new
38
- THREAD = Thread.new do
39
- while true
40
- method, i = QUEUE.pop
41
- case method
42
- when :thread_local_finalizer
43
- LOCK.synchronize do
44
- FREE.push(i)
45
- # The cost of GC'ing a TLV is linear in the number of threads using TLVs
46
- # But that is natural! More threads means more storage is used per TLV
47
- # So naturally more CPU time is required to free more storage
48
- ARRAYS.each_value do |array|
49
- array[i] = nil
50
- end
51
- end
52
- when :thread_finalizer
53
- LOCK.synchronize do
54
- # The thread which used this thread-local array is now gone
55
- # So don't hold onto a reference to the array (thus blocking GC)
56
- ARRAYS.delete(i)
57
- end
58
- end
31
+ FREE = []
32
+ LOCK = Mutex.new
33
+ THREAD_LOCAL_ARRAYS = {} # used as a hash set
34
+
35
+ # synchronize when not on MRI
36
+ # on MRI using lock in finalizer leads to "can't be called from trap context" error
37
+ # so the code is carefully written to be tread-safe on MRI relying on GIL
38
+
39
+ if Concurrent.on_cruby?
40
+ # @!visibility private
41
+ def self.semi_sync(&block)
42
+ block.call
43
+ end
44
+ else
45
+ # @!visibility private
46
+ def self.semi_sync(&block)
47
+ LOCK.synchronize(&block)
59
48
  end
60
49
  end
61
50
 
62
- private_constant :FREE, :LOCK, :ARRAYS, :QUEUE, :THREAD
51
+ private_constant :FREE, :LOCK, :THREAD_LOCAL_ARRAYS
63
52
 
64
53
  # @!macro thread_local_var_method_get
65
54
  def value
@@ -85,7 +74,7 @@ module Concurrent
85
74
  # Using Ruby's built-in thread-local storage is faster
86
75
  unless (array = get_threadlocal_array(me))
87
76
  array = set_threadlocal_array([], me)
88
- LOCK.synchronize { ARRAYS[array.object_id] = array }
77
+ self.class.semi_sync { THREAD_LOCAL_ARRAYS[array.object_id] = array }
89
78
  ObjectSpace.define_finalizer(me, self.class.thread_finalizer(array.object_id))
90
79
  end
91
80
  array[@index] = (value.nil? ? NULL : value)
@@ -95,32 +84,53 @@ module Concurrent
95
84
  protected
96
85
 
97
86
  # @!visibility private
98
- # noinspection RubyClassVariableUsageInspection
99
87
  def allocate_storage
100
- @index = LOCK.synchronize do
101
- FREE.pop || begin
102
- result = @@next
103
- @@next += 1
104
- result
105
- end
106
- end
88
+ @index = FREE.pop || next_index
89
+
107
90
  ObjectSpace.define_finalizer(self, self.class.thread_local_finalizer(@index))
108
91
  end
109
92
 
110
93
  # @!visibility private
111
94
  def self.thread_local_finalizer(index)
112
- # avoid error: can't be called from trap context
113
- proc { QUEUE.push [:thread_local_finalizer, index] }
95
+ proc do
96
+ semi_sync do
97
+ # The cost of GC'ing a TLV is linear in the number of threads using TLVs
98
+ # But that is natural! More threads means more storage is used per TLV
99
+ # So naturally more CPU time is required to free more storage
100
+ #
101
+ # DO NOT use each_value which might conflict with new pair assignment
102
+ # into the hash in #value= method
103
+ THREAD_LOCAL_ARRAYS.values.each { |array| array[index] = nil }
104
+ # free index has to be published after the arrays are cleared
105
+ FREE.push(index)
106
+ end
107
+ end
114
108
  end
115
109
 
116
110
  # @!visibility private
117
111
  def self.thread_finalizer(id)
118
- # avoid error: can't be called from trap context
119
- proc { QUEUE.push [:thread_finalizer, id] }
112
+ proc do
113
+ semi_sync do
114
+ # The thread which used this thread-local array is now gone
115
+ # So don't hold onto a reference to the array (thus blocking GC)
116
+ THREAD_LOCAL_ARRAYS.delete(id)
117
+ end
118
+ end
120
119
  end
121
120
 
122
121
  private
123
122
 
123
+ # noinspection RubyClassVariableUsageInspection
124
+ @@next = 0
125
+ # noinspection RubyClassVariableUsageInspection
126
+ def next_index
127
+ LOCK.synchronize do
128
+ result = @@next
129
+ @@next += 1
130
+ result
131
+ end
132
+ end
133
+
124
134
  if Thread.instance_methods.include?(:thread_variable_get)
125
135
 
126
136
  def get_threadlocal_array(thread = Thread.current)
@@ -16,14 +16,16 @@ module Concurrent
16
16
  # @!macro semaphore_method_acquire
17
17
  #
18
18
  # Acquires the given number of permits from this semaphore,
19
- # blocking until all are available.
19
+ # blocking until all are available. If a block is given,
20
+ # yields to it and releases the permits afterwards.
20
21
  #
21
22
  # @param [Fixnum] permits Number of permits to acquire
22
23
  #
23
24
  # @raise [ArgumentError] if `permits` is not an integer or is less than
24
25
  # one
25
26
  #
26
- # @return [nil]
27
+ # @return [nil, BasicObject] Without a block, `nil` is returned. If a block
28
+ # is given, its return value is returned.
27
29
 
28
30
  # @!macro semaphore_method_available_permits
29
31
  #
@@ -41,7 +43,9 @@ module Concurrent
41
43
  #
42
44
  # Acquires the given number of permits from this semaphore,
43
45
  # only if all are available at the time of invocation or within
44
- # `timeout` interval
46
+ # `timeout` interval. If a block is given, yields to it if the permits
47
+ # were successfully acquired, and releases them afterward, returning the
48
+ # block's return value.
45
49
  #
46
50
  # @param [Fixnum] permits the number of permits to acquire
47
51
  #
@@ -51,8 +55,10 @@ module Concurrent
51
55
  # @raise [ArgumentError] if `permits` is not an integer or is less than
52
56
  # one
53
57
  #
54
- # @return [Boolean] `false` if no permits are available, `true` when
55
- # acquired a permit
58
+ # @return [true, false, nil, BasicObject] `false` if no permits are
59
+ # available, `true` when acquired a permit. If a block is given, the
60
+ # block's return value is returned if the permits were acquired; if not,
61
+ # `nil` is returned.
56
62
 
57
63
  # @!macro semaphore_method_release
58
64
  #
@@ -106,6 +112,8 @@ module Concurrent
106
112
  # releasing a blocking acquirer.
107
113
  # However, no actual permit objects are used; the Semaphore just keeps a
108
114
  # count of the number available and acts accordingly.
115
+ # Alternatively, permits may be acquired within a block, and automatically
116
+ # released after the block finishes executing.
109
117
  #
110
118
  # @!macro semaphore_public_api
111
119
  # @example
@@ -140,6 +148,19 @@ module Concurrent
140
148
  # # Thread 4 releasing semaphore
141
149
  # # Thread 1 acquired semaphore
142
150
  #
151
+ # @example
152
+ # semaphore = Concurrent::Semaphore.new(1)
153
+ #
154
+ # puts semaphore.available_permits
155
+ # semaphore.acquire do
156
+ # puts semaphore.available_permits
157
+ # end
158
+ # puts semaphore.available_permits
159
+ #
160
+ # # prints:
161
+ # # 1
162
+ # # 0
163
+ # # 1
143
164
  class Semaphore < SemaphoreImplementation
144
165
  end
145
166
  end
@@ -19,7 +19,7 @@ module Concurrent
19
19
  end
20
20
 
21
21
  def compute_if_absent(key)
22
- if stored_value = _get(key) # fast non-blocking path for the most likely case
22
+ if NULL != (stored_value = @backend.fetch(key, NULL)) # fast non-blocking path for the most likely case
23
23
  stored_value
24
24
  else
25
25
  @write_lock.synchronize { super }
@@ -0,0 +1,14 @@
1
+ module Concurrent
2
+
3
+ # @!visibility private
4
+ module Collection
5
+
6
+ # @!visibility private
7
+ class TruffleRubyMapBackend < TruffleRuby::ConcurrentMap
8
+ def initialize(options = nil)
9
+ options ||= {}
10
+ super(initial_capacity: options[:initial_capacity], load_factor: options[:load_factor])
11
+ end
12
+ end
13
+ end
14
+ end
@@ -30,7 +30,7 @@ module Concurrent
30
30
  if @queue[k] == item
31
31
  swap(k, @length)
32
32
  @length -= 1
33
- sink(k)
33
+ sink(k) || swim(k)
34
34
  @queue.pop
35
35
  else
36
36
  k += 1
@@ -126,12 +126,17 @@ module Concurrent
126
126
  #
127
127
  # @!visibility private
128
128
  def sink(k)
129
+ success = false
130
+
129
131
  while (j = (2 * k)) <= @length do
130
132
  j += 1 if j < @length && ! ordered?(j, j+1)
131
133
  break if ordered?(k, j)
132
134
  swap(k, j)
135
+ success = true
133
136
  k = j
134
137
  end
138
+
139
+ success
135
140
  end
136
141
 
137
142
  # Percolate up to maintain heap invariant.
@@ -140,10 +145,15 @@ module Concurrent
140
145
  #
141
146
  # @!visibility private
142
147
  def swim(k)
148
+ success = false
149
+
143
150
  while k > 1 && ! ordered?(k/2, k) do
144
151
  swap(k, k/2)
145
152
  k = k/2
153
+ success = true
146
154
  end
155
+
156
+ success
147
157
  end
148
158
  end
149
159
  end
@@ -75,28 +75,31 @@ module Concurrent
75
75
 
76
76
  private
77
77
 
78
- # Handler which executes the `fallback_policy` once the queue size
79
- # reaches `max_queue`.
78
+ # Returns an action which executes the `fallback_policy` once the queue
79
+ # size reaches `max_queue`. The reason for the indirection of an action
80
+ # is so that the work can be deferred outside of synchronization.
80
81
  #
81
82
  # @param [Array] args the arguments to the task which is being handled.
82
83
  #
83
84
  # @!visibility private
84
- def handle_fallback(*args)
85
+ def fallback_action(*args)
85
86
  case fallback_policy
86
87
  when :abort
87
- raise RejectedExecutionError
88
+ lambda { raise RejectedExecutionError }
88
89
  when :discard
89
- false
90
+ lambda { false }
90
91
  when :caller_runs
91
- begin
92
- yield(*args)
93
- rescue => ex
94
- # let it fail
95
- log DEBUG, ex
96
- end
97
- true
92
+ lambda {
93
+ begin
94
+ yield(*args)
95
+ rescue => ex
96
+ # let it fail
97
+ log DEBUG, ex
98
+ end
99
+ true
100
+ }
98
101
  else
99
- fail "Unknown fallback policy #{fallback_policy}"
102
+ lambda { fail "Unknown fallback policy #{fallback_policy}" }
100
103
  end
101
104
  end
102
105
 
@@ -16,6 +16,9 @@ module Concurrent
16
16
  # Default maximum number of seconds a thread in the pool may remain idle
17
17
  # before being reclaimed.
18
18
 
19
+ # @!macro thread_pool_executor_constant_default_synchronous
20
+ # Default value of the :synchronous option.
21
+
19
22
  # @!macro thread_pool_executor_attr_reader_max_length
20
23
  # The maximum number of threads that may be created in the pool.
21
24
  # @return [Integer] The maximum number of threads that may be created in the pool.
@@ -40,6 +43,10 @@ module Concurrent
40
43
  # The number of seconds that a thread may be idle before being reclaimed.
41
44
  # @return [Integer] The number of seconds that a thread may be idle before being reclaimed.
42
45
 
46
+ # @!macro thread_pool_executor_attr_reader_synchronous
47
+ # Whether or not a value of 0 for :max_queue option means the queue must perform direct hand-off or rather unbounded queue.
48
+ # @return [true, false]
49
+
43
50
  # @!macro thread_pool_executor_attr_reader_max_queue
44
51
  # The maximum number of tasks that may be waiting in the work queue at any one time.
45
52
  # When the queue size reaches `max_queue` subsequent tasks will be rejected in
@@ -64,9 +71,16 @@ module Concurrent
64
71
  # @return [Integer] Number of tasks that may be enqueued before reaching `max_queue` and rejecting
65
72
  # new tasks. A value of -1 indicates that the queue may grow without bound.
66
73
 
67
-
68
-
69
-
74
+ # @!macro thread_pool_executor_method_prune_pool
75
+ # Prune the thread pool of unneeded threads
76
+ #
77
+ # What is being pruned is controlled by the min_threads and idletime
78
+ # parameters passed at pool creation time
79
+ #
80
+ # This is a no-op on some pool implementation (e.g. the Java one). The Ruby
81
+ # pool will auto-prune each time a new job is posted. You will need to call
82
+ # this method explicitely in case your application post jobs in bursts (a
83
+ # lot of jobs and then nothing for long periods)
70
84
 
71
85
  # @!macro thread_pool_executor_public_api
72
86
  #
@@ -104,6 +118,9 @@ module Concurrent
104
118
  #
105
119
  # @!method can_overflow?
106
120
  # @!macro executor_service_method_can_overflow_question
121
+ #
122
+ # @!method prune_pool
123
+ # @!macro thread_pool_executor_method_prune_pool
107
124
 
108
125
 
109
126
 
@@ -20,7 +20,7 @@ if Concurrent.on_jruby?
20
20
 
21
21
  def post(*args, &task)
22
22
  raise ArgumentError.new('no block given') unless block_given?
23
- return handle_fallback(*args, &task) unless running?
23
+ return fallback_action(*args, &task).call unless running?
24
24
  @executor.submit Job.new(args, task)
25
25
  true
26
26
  rescue Java::JavaUtilConcurrent::RejectedExecutionException
@@ -21,12 +21,18 @@ if Concurrent.on_jruby?
21
21
  # @!macro thread_pool_executor_constant_default_thread_timeout
22
22
  DEFAULT_THREAD_IDLETIMEOUT = 60
23
23
 
24
+ # @!macro thread_pool_executor_constant_default_synchronous
25
+ DEFAULT_SYNCHRONOUS = false
26
+
24
27
  # @!macro thread_pool_executor_attr_reader_max_length
25
28
  attr_reader :max_length
26
29
 
27
30
  # @!macro thread_pool_executor_attr_reader_max_queue
28
31
  attr_reader :max_queue
29
32
 
33
+ # @!macro thread_pool_executor_attr_reader_synchronous
34
+ attr_reader :synchronous
35
+
30
36
  # @!macro thread_pool_executor_method_initialize
31
37
  def initialize(opts = {})
32
38
  super(opts)
@@ -87,6 +93,10 @@ if Concurrent.on_jruby?
87
93
  super && !@executor.isTerminating
88
94
  end
89
95
 
96
+ # @!macro thread_pool_executor_method_prune_pool
97
+ def prune_pool
98
+ end
99
+
90
100
  private
91
101
 
92
102
  def ns_initialize(opts)
@@ -94,8 +104,10 @@ if Concurrent.on_jruby?
94
104
  max_length = opts.fetch(:max_threads, DEFAULT_MAX_POOL_SIZE).to_i
95
105
  idletime = opts.fetch(:idletime, DEFAULT_THREAD_IDLETIMEOUT).to_i
96
106
  @max_queue = opts.fetch(:max_queue, DEFAULT_MAX_QUEUE_SIZE).to_i
107
+ @synchronous = opts.fetch(:synchronous, DEFAULT_SYNCHRONOUS)
97
108
  @fallback_policy = opts.fetch(:fallback_policy, :abort)
98
109
 
110
+ raise ArgumentError.new("`synchronous` cannot be set unless `max_queue` is 0") if @synchronous && @max_queue > 0
99
111
  raise ArgumentError.new("`max_threads` cannot be less than #{DEFAULT_MIN_POOL_SIZE}") if max_length < DEFAULT_MIN_POOL_SIZE
100
112
  raise ArgumentError.new("`max_threads` cannot be greater than #{DEFAULT_MAX_POOL_SIZE}") if max_length > DEFAULT_MAX_POOL_SIZE
101
113
  raise ArgumentError.new("`min_threads` cannot be less than #{DEFAULT_MIN_POOL_SIZE}") if min_length < DEFAULT_MIN_POOL_SIZE
@@ -103,7 +115,11 @@ if Concurrent.on_jruby?
103
115
  raise ArgumentError.new("#{fallback_policy} is not a valid fallback policy") unless FALLBACK_POLICY_CLASSES.include?(@fallback_policy)
104
116
 
105
117
  if @max_queue == 0
106
- queue = java.util.concurrent.LinkedBlockingQueue.new
118
+ if @synchronous
119
+ queue = java.util.concurrent.SynchronousQueue.new
120
+ else
121
+ queue = java.util.concurrent.LinkedBlockingQueue.new
122
+ end
107
123
  else
108
124
  queue = java.util.concurrent.LinkedBlockingQueue.new(@max_queue)
109
125
  end
@@ -16,10 +16,16 @@ module Concurrent
16
16
 
17
17
  def post(*args, &task)
18
18
  raise ArgumentError.new('no block given') unless block_given?
19
- synchronize do
20
- # If the executor is shut down, reject this task
21
- return handle_fallback(*args, &task) unless running?
22
- ns_execute(*args, &task)
19
+ deferred_action = synchronize {
20
+ if running?
21
+ ns_execute(*args, &task)
22
+ else
23
+ fallback_action(*args, &task)
24
+ end
25
+ }
26
+ if deferred_action
27
+ deferred_action.call
28
+ else
23
29
  true
24
30
  end
25
31
  end