concurrent-ruby 1.1.6 → 1.1.10

Sign up to get free protection for your applications and to get access to all the features.
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