concurrent-ruby 0.5.0 → 0.6.0.pre.1
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.
- checksums.yaml +4 -4
- data/README.md +88 -77
- data/lib/concurrent.rb +17 -2
- data/lib/concurrent/actor.rb +17 -0
- data/lib/concurrent/actor_context.rb +31 -0
- data/lib/concurrent/actor_ref.rb +39 -0
- data/lib/concurrent/agent.rb +12 -3
- data/lib/concurrent/async.rb +290 -0
- data/lib/concurrent/atomic.rb +5 -9
- data/lib/concurrent/cached_thread_pool.rb +39 -137
- data/lib/concurrent/channel/blocking_ring_buffer.rb +60 -0
- data/lib/concurrent/channel/buffered_channel.rb +83 -0
- data/lib/concurrent/channel/channel.rb +11 -0
- data/lib/concurrent/channel/probe.rb +19 -0
- data/lib/concurrent/channel/ring_buffer.rb +54 -0
- data/lib/concurrent/channel/unbuffered_channel.rb +34 -0
- data/lib/concurrent/channel/waitable_list.rb +38 -0
- data/lib/concurrent/configuration.rb +92 -0
- data/lib/concurrent/dataflow.rb +9 -3
- data/lib/concurrent/delay.rb +88 -0
- data/lib/concurrent/exchanger.rb +31 -0
- data/lib/concurrent/fixed_thread_pool.rb +28 -122
- data/lib/concurrent/future.rb +10 -5
- data/lib/concurrent/immediate_executor.rb +3 -2
- data/lib/concurrent/ivar.rb +2 -1
- data/lib/concurrent/java_cached_thread_pool.rb +45 -0
- data/lib/concurrent/java_fixed_thread_pool.rb +37 -0
- data/lib/concurrent/java_thread_pool_executor.rb +194 -0
- data/lib/concurrent/per_thread_executor.rb +23 -0
- data/lib/concurrent/postable.rb +2 -0
- data/lib/concurrent/processor_count.rb +125 -0
- data/lib/concurrent/promise.rb +42 -18
- data/lib/concurrent/ruby_cached_thread_pool.rb +37 -0
- data/lib/concurrent/ruby_fixed_thread_pool.rb +31 -0
- data/lib/concurrent/ruby_thread_pool_executor.rb +268 -0
- data/lib/concurrent/ruby_thread_pool_worker.rb +69 -0
- data/lib/concurrent/simple_actor_ref.rb +124 -0
- data/lib/concurrent/thread_local_var.rb +1 -1
- data/lib/concurrent/thread_pool_executor.rb +30 -0
- data/lib/concurrent/timer_task.rb +13 -10
- data/lib/concurrent/tvar.rb +212 -0
- data/lib/concurrent/utilities.rb +1 -0
- data/lib/concurrent/version.rb +1 -1
- data/spec/concurrent/actor_context_spec.rb +37 -0
- data/spec/concurrent/actor_ref_shared.rb +313 -0
- data/spec/concurrent/actor_spec.rb +9 -1
- data/spec/concurrent/agent_spec.rb +97 -96
- data/spec/concurrent/async_spec.rb +320 -0
- data/spec/concurrent/cached_thread_pool_shared.rb +137 -0
- data/spec/concurrent/channel/blocking_ring_buffer_spec.rb +149 -0
- data/spec/concurrent/channel/buffered_channel_spec.rb +151 -0
- data/spec/concurrent/channel/channel_spec.rb +37 -0
- data/spec/concurrent/channel/probe_spec.rb +49 -0
- data/spec/concurrent/channel/ring_buffer_spec.rb +126 -0
- data/spec/concurrent/channel/unbuffered_channel_spec.rb +132 -0
- data/spec/concurrent/configuration_spec.rb +134 -0
- data/spec/concurrent/dataflow_spec.rb +109 -27
- data/spec/concurrent/delay_spec.rb +77 -0
- data/spec/concurrent/exchanger_spec.rb +66 -0
- data/spec/concurrent/fixed_thread_pool_shared.rb +136 -0
- data/spec/concurrent/future_spec.rb +60 -51
- data/spec/concurrent/global_thread_pool_shared.rb +33 -0
- data/spec/concurrent/immediate_executor_spec.rb +4 -25
- data/spec/concurrent/ivar_spec.rb +36 -23
- data/spec/concurrent/java_cached_thread_pool_spec.rb +64 -0
- data/spec/concurrent/java_fixed_thread_pool_spec.rb +64 -0
- data/spec/concurrent/java_thread_pool_executor_spec.rb +71 -0
- data/spec/concurrent/obligation_shared.rb +32 -20
- data/spec/concurrent/{global_thread_pool_spec.rb → per_thread_executor_spec.rb} +9 -13
- data/spec/concurrent/processor_count_spec.rb +20 -0
- data/spec/concurrent/promise_spec.rb +29 -41
- data/spec/concurrent/ruby_cached_thread_pool_spec.rb +69 -0
- data/spec/concurrent/ruby_fixed_thread_pool_spec.rb +39 -0
- data/spec/concurrent/ruby_thread_pool_executor_spec.rb +183 -0
- data/spec/concurrent/simple_actor_ref_spec.rb +219 -0
- data/spec/concurrent/thread_pool_class_cast_spec.rb +40 -0
- data/spec/concurrent/thread_pool_executor_shared.rb +155 -0
- data/spec/concurrent/thread_pool_shared.rb +98 -36
- data/spec/concurrent/tvar_spec.rb +137 -0
- data/spec/spec_helper.rb +4 -0
- data/spec/support/functions.rb +4 -0
- metadata +85 -20
- data/lib/concurrent/cached_thread_pool/worker.rb +0 -91
- data/lib/concurrent/channel.rb +0 -63
- data/lib/concurrent/fixed_thread_pool/worker.rb +0 -54
- data/lib/concurrent/global_thread_pool.rb +0 -42
- data/spec/concurrent/cached_thread_pool_spec.rb +0 -101
- data/spec/concurrent/channel_spec.rb +0 -86
- data/spec/concurrent/fixed_thread_pool_spec.rb +0 -92
- data/spec/concurrent/uses_global_thread_pool_shared.rb +0 -64
@@ -0,0 +1,23 @@
|
|
1
|
+
module Concurrent
|
2
|
+
|
3
|
+
class PerThreadExecutor
|
4
|
+
|
5
|
+
def self.post(*args)
|
6
|
+
raise ArgumentError.new('no block given') unless block_given?
|
7
|
+
Thread.new(*args) do
|
8
|
+
Thread.current.abort_on_exception = false
|
9
|
+
yield(*args)
|
10
|
+
end
|
11
|
+
return true
|
12
|
+
end
|
13
|
+
|
14
|
+
def post(*args, &block)
|
15
|
+
return PerThreadExecutor.post(*args, &block)
|
16
|
+
end
|
17
|
+
|
18
|
+
def <<(block)
|
19
|
+
PerThreadExecutor.post(&block)
|
20
|
+
return self
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/lib/concurrent/postable.rb
CHANGED
@@ -75,6 +75,7 @@ module Concurrent
|
|
75
75
|
end
|
76
76
|
end
|
77
77
|
|
78
|
+
# @deprecated +Actor+ is being replaced with a completely new framework prior to v1.0.0
|
78
79
|
def forward(receiver, *message)
|
79
80
|
raise ArgumentError.new('empty message') if message.empty?
|
80
81
|
return false unless ready?
|
@@ -82,6 +83,7 @@ module Concurrent
|
|
82
83
|
queue.length
|
83
84
|
end
|
84
85
|
|
86
|
+
# @deprecated +Actor+ is being replaced with a completely new framework prior to v1.0.0
|
85
87
|
def ready?
|
86
88
|
if self.respond_to?(:running?) && ! running?
|
87
89
|
false
|
@@ -0,0 +1,125 @@
|
|
1
|
+
require 'rbconfig'
|
2
|
+
|
3
|
+
module Concurrent
|
4
|
+
|
5
|
+
# Number of processors seen by the OS and used for process scheduling. For performance
|
6
|
+
# reasons the calculated value will be memoized on the first call.
|
7
|
+
#
|
8
|
+
# When running under JRuby the Java runtime call +java.lang.Runtime.getRuntime.availableProcessors+
|
9
|
+
# will be used. According to the Java documentation this "value may change
|
10
|
+
# during a particular invocation of the virtual machine... [applications]
|
11
|
+
# should therefore occasionally poll this property." Subsequently the result
|
12
|
+
# will NOT be memoized under JRuby.
|
13
|
+
#
|
14
|
+
# On Windows the Win32 API will be queried for the `NumberOfLogicalProcessors from Win32_Processor`.
|
15
|
+
# This will return the total number "logical processors for the current instance of the processor",
|
16
|
+
# which taked into account hyperthreading.
|
17
|
+
#
|
18
|
+
# * AIX: /usr/sbin/pmcycles (AIX 5+), /usr/sbin/lsdev
|
19
|
+
# * BSD: /sbin/sysctl
|
20
|
+
# * Cygwin: /proc/cpuinfo
|
21
|
+
# * Darwin: /usr/bin/hwprefs, /usr/sbin/sysctl
|
22
|
+
# * HP-UX: /usr/sbin/ioscan
|
23
|
+
# * IRIX: /usr/sbin/sysconf
|
24
|
+
# * Linux: /proc/cpuinfo
|
25
|
+
# * Minix 3+: /proc/cpuinfo
|
26
|
+
# * Solaris: /usr/sbin/psrinfo
|
27
|
+
# * Tru64 UNIX: /usr/sbin/psrinfo
|
28
|
+
# * UnixWare: /usr/sbin/psrinfo
|
29
|
+
#
|
30
|
+
# @return [Integer] number of processors seen by the OS or Java runtime
|
31
|
+
#
|
32
|
+
# @see https://github.com/grosser/parallel/blob/4fc8b89d08c7091fe0419ca8fba1ec3ce5a8d185/lib/parallel.rb
|
33
|
+
#
|
34
|
+
# @see http://docs.oracle.com/javase/6/docs/api/java/lang/Runtime.html#availableProcessors()
|
35
|
+
# @see http://msdn.microsoft.com/en-us/library/aa394373(v=vs.85).aspx
|
36
|
+
def processor_count
|
37
|
+
if RUBY_PLATFORM == 'java'
|
38
|
+
java.lang.Runtime.getRuntime.availableProcessors
|
39
|
+
else
|
40
|
+
@@processor_count ||= begin
|
41
|
+
os_name = RbConfig::CONFIG["target_os"]
|
42
|
+
if os_name =~ /mingw|mswin/
|
43
|
+
require 'win32ole'
|
44
|
+
result = WIN32OLE.connect("winmgmts://").ExecQuery(
|
45
|
+
"select NumberOfLogicalProcessors from Win32_Processor")
|
46
|
+
result.to_enum.collect(&:NumberOfLogicalProcessors).reduce(:+)
|
47
|
+
elsif File.readable?("/proc/cpuinfo")
|
48
|
+
IO.read("/proc/cpuinfo").scan(/^processor/).size
|
49
|
+
elsif File.executable?("/usr/bin/hwprefs")
|
50
|
+
IO.popen("/usr/bin/hwprefs thread_count").read.to_i
|
51
|
+
elsif File.executable?("/usr/sbin/psrinfo")
|
52
|
+
IO.popen("/usr/sbin/psrinfo").read.scan(/^.*on-*line/).size
|
53
|
+
elsif File.executable?("/usr/sbin/ioscan")
|
54
|
+
IO.popen("/usr/sbin/ioscan -kC processor") do |out|
|
55
|
+
out.read.scan(/^.*processor/).size
|
56
|
+
end
|
57
|
+
elsif File.executable?("/usr/sbin/pmcycles")
|
58
|
+
IO.popen("/usr/sbin/pmcycles -m").read.count("\n")
|
59
|
+
elsif File.executable?("/usr/sbin/lsdev")
|
60
|
+
IO.popen("/usr/sbin/lsdev -Cc processor -S 1").read.count("\n")
|
61
|
+
elsif File.executable?("/usr/sbin/sysconf") and os_name =~ /irix/i
|
62
|
+
IO.popen("/usr/sbin/sysconf NPROC_ONLN").read.to_i
|
63
|
+
elsif File.executable?("/usr/sbin/sysctl")
|
64
|
+
IO.popen("/usr/sbin/sysctl -n hw.ncpu").read.to_i
|
65
|
+
elsif File.executable?("/sbin/sysctl")
|
66
|
+
IO.popen("/sbin/sysctl -n hw.ncpu").read.to_i
|
67
|
+
else
|
68
|
+
1
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
rescue
|
73
|
+
return 1
|
74
|
+
end
|
75
|
+
module_function :processor_count
|
76
|
+
|
77
|
+
# Number of physical processor cores on the current system. For performance reasons
|
78
|
+
# the calculated value will be memoized on the first call.
|
79
|
+
#
|
80
|
+
# On Windows the Win32 API will be queried for the `NumberOfCores from Win32_Processor`.
|
81
|
+
# This will return the total number "of cores for the current instance of the processor."
|
82
|
+
# On Unix-like operating systems either the `hwprefs` or `sysctl` utility will be called
|
83
|
+
# in a subshell and the returned value will be used. In the rare case where none of these
|
84
|
+
# methods work or an exception is raised the function will simply return 1.
|
85
|
+
#
|
86
|
+
# @return [Integer] number physical processor cores on the current system
|
87
|
+
#
|
88
|
+
# @see https://github.com/grosser/parallel/blob/4fc8b89d08c7091fe0419ca8fba1ec3ce5a8d185/lib/parallel.rb
|
89
|
+
#
|
90
|
+
# @see http://msdn.microsoft.com/en-us/library/aa394373(v=vs.85).aspx
|
91
|
+
# @see http://www.unix.com/man-page/osx/1/HWPREFS/
|
92
|
+
# @see http://linux.die.net/man/8/sysctl
|
93
|
+
def physical_processor_count
|
94
|
+
@@physical_processor_count ||= begin
|
95
|
+
ppc = case RbConfig::CONFIG["target_os"]
|
96
|
+
when /darwin1/
|
97
|
+
IO.popen("/usr/sbin/sysctl -n hw.physicalcpu").read.to_i
|
98
|
+
when /linux/
|
99
|
+
cores = {} # unique physical ID / core ID combinations
|
100
|
+
phy = 0
|
101
|
+
IO.read("/proc/cpuinfo").scan(/^physical id.*|^core id.*/) do |ln|
|
102
|
+
if ln.start_with?("physical")
|
103
|
+
phy = ln[/\d+/]
|
104
|
+
elsif ln.start_with?("core")
|
105
|
+
cid = phy + ":" + ln[/\d+/]
|
106
|
+
cores[cid] = true if not cores[cid]
|
107
|
+
end
|
108
|
+
end
|
109
|
+
cores.count
|
110
|
+
when /mswin|mingw/
|
111
|
+
require 'win32ole'
|
112
|
+
result_set = WIN32OLE.connect("winmgmts://").ExecQuery(
|
113
|
+
"select NumberOfCores from Win32_Processor")
|
114
|
+
result_set.to_enum.collect(&:NumberOfCores).reduce(:+)
|
115
|
+
else
|
116
|
+
processor_count
|
117
|
+
end
|
118
|
+
# fall back to logical count if physical info is invalid
|
119
|
+
ppc > 0 ? ppc : processor_count
|
120
|
+
end
|
121
|
+
rescue
|
122
|
+
return 1
|
123
|
+
end
|
124
|
+
module_function :physical_processor_count
|
125
|
+
end
|
data/lib/concurrent/promise.rb
CHANGED
@@ -1,22 +1,43 @@
|
|
1
1
|
require 'thread'
|
2
2
|
|
3
|
-
require 'concurrent/
|
3
|
+
require 'concurrent/configuration'
|
4
4
|
require 'concurrent/obligation'
|
5
5
|
|
6
6
|
module Concurrent
|
7
7
|
|
8
8
|
class Promise
|
9
9
|
include Obligation
|
10
|
-
include
|
11
|
-
|
10
|
+
include OptionsParser
|
11
|
+
|
12
|
+
# Initialize a new Promise with the provided options.
|
13
|
+
#
|
14
|
+
# @param [Object] initial the initial value
|
15
|
+
# @param [Hash] opts the options used to define the behavior at update and deref
|
16
|
+
#
|
17
|
+
# @option opts [Promise] :parent the parent +Promise+ when building a chain/tree
|
18
|
+
# @option opts [Proc] :on_fulfill fulfillment handler
|
19
|
+
# @option opts [Proc] :on_reject rejection handler
|
20
|
+
#
|
21
|
+
# @option opts [Boolean] :operation (false) when +true+ will execute the future on the global
|
22
|
+
# operation pool (for long-running operations), when +false+ will execute the future on the
|
23
|
+
# global task pool (for short-running tasks)
|
24
|
+
# @option opts [object] :executor when provided will run all operations on
|
25
|
+
# this executor rather than the global thread pool (overrides :operation)
|
26
|
+
#
|
27
|
+
# @option opts [String] :dup_on_deref (false) call +#dup+ before returning the data
|
28
|
+
# @option opts [String] :freeze_on_deref (false) call +#freeze+ before returning the data
|
29
|
+
# @option opts [String] :copy_on_deref (nil) call the given +Proc+ passing the internal value and
|
30
|
+
# returning the value returned from the proc
|
31
|
+
#
|
12
32
|
# @see http://wiki.commonjs.org/wiki/Promises/A
|
13
33
|
# @see http://promises-aplus.github.io/promises-spec/
|
14
|
-
def initialize(
|
15
|
-
|
34
|
+
def initialize(opts = {}, &block)
|
35
|
+
opts.delete_if {|k, v| v.nil?}
|
16
36
|
|
17
|
-
@
|
18
|
-
@
|
19
|
-
@
|
37
|
+
@executor = get_executor_from(opts)
|
38
|
+
@parent = opts.fetch(:parent) { nil }
|
39
|
+
@on_fulfill = opts.fetch(:on_fulfill) { Proc.new{ |result| result } }
|
40
|
+
@on_reject = opts.fetch(:on_reject) { Proc.new{ |reason| raise reason } }
|
20
41
|
|
21
42
|
@promise_body = block || Proc.new{|result| result }
|
22
43
|
@state = :unscheduled
|
@@ -26,14 +47,14 @@ module Concurrent
|
|
26
47
|
end
|
27
48
|
|
28
49
|
# @return [Promise]
|
29
|
-
def self.fulfill(value)
|
30
|
-
Promise.new.tap
|
50
|
+
def self.fulfill(value, opts = {})
|
51
|
+
Promise.new(opts).tap{ |p| p.send(:synchronized_set_state!, true, value, nil) }
|
31
52
|
end
|
32
53
|
|
33
54
|
|
34
55
|
# @return [Promise]
|
35
|
-
def self.reject(reason)
|
36
|
-
Promise.new.tap
|
56
|
+
def self.reject(reason, opts = {})
|
57
|
+
Promise.new(opts).tap{ |p| p.send(:synchronized_set_state!, false, nil, reason) }
|
37
58
|
end
|
38
59
|
|
39
60
|
# @return [Promise]
|
@@ -51,16 +72,20 @@ module Concurrent
|
|
51
72
|
end
|
52
73
|
|
53
74
|
# @since 0.5.0
|
54
|
-
def self.execute(&block)
|
55
|
-
new(&block).execute
|
75
|
+
def self.execute(opts = {}, &block)
|
76
|
+
new(opts, &block).execute
|
56
77
|
end
|
57
78
|
|
58
|
-
|
59
79
|
# @return [Promise] the new promise
|
60
80
|
def then(rescuer = nil, &block)
|
61
81
|
raise ArgumentError.new('rescuers and block are both missing') if rescuer.nil? && !block_given?
|
62
82
|
block = Proc.new{ |result| result } if block.nil?
|
63
|
-
child = Promise.new(
|
83
|
+
child = Promise.new(
|
84
|
+
parent: self,
|
85
|
+
executor: @executor,
|
86
|
+
on_fulfill: block,
|
87
|
+
on_reject: rescuer
|
88
|
+
)
|
64
89
|
|
65
90
|
mutex.synchronize do
|
66
91
|
child.state = :pending if @state == :pending
|
@@ -118,7 +143,7 @@ module Concurrent
|
|
118
143
|
|
119
144
|
# @!visibility private
|
120
145
|
def realize(task)
|
121
|
-
|
146
|
+
@executor.post do
|
122
147
|
success, value, reason = SafeTaskExecutor.new( task ).execute
|
123
148
|
|
124
149
|
children_to_notify = mutex.synchronize do
|
@@ -140,6 +165,5 @@ module Concurrent
|
|
140
165
|
set_state!(success, value, reason)
|
141
166
|
end
|
142
167
|
end
|
143
|
-
|
144
168
|
end
|
145
169
|
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'concurrent/ruby_thread_pool_executor'
|
2
|
+
|
3
|
+
module Concurrent
|
4
|
+
|
5
|
+
# @!macro cached_thread_pool
|
6
|
+
class RubyCachedThreadPool < RubyThreadPoolExecutor
|
7
|
+
|
8
|
+
# Create a new thread pool.
|
9
|
+
#
|
10
|
+
# @param [Hash] opts the options defining pool behavior.
|
11
|
+
# @option opts [Integer] :max_threads (+DEFAULT_MAX_POOL_SIZE+) maximum number
|
12
|
+
# of threads which may be created in the pool
|
13
|
+
# @option opts [Integer] :idletime (+DEFAULT_THREAD_IDLETIMEOUT+) maximum
|
14
|
+
# number of seconds a thread may be idle before it is reclaimed
|
15
|
+
#
|
16
|
+
# @raise [ArgumentError] if +max_threads+ is less than or equal to zero
|
17
|
+
# @raise [ArgumentError] if +idletime+ is less than or equal to zero
|
18
|
+
# @raise [ArgumentError] if +overflow_policy+ is not a known policy
|
19
|
+
def initialize(opts = {})
|
20
|
+
max_length = opts.fetch(:max_threads, DEFAULT_MAX_POOL_SIZE).to_i
|
21
|
+
idletime = opts.fetch(:idletime, DEFAULT_THREAD_IDLETIMEOUT).to_i
|
22
|
+
overflow_policy = opts.fetch(:overflow_policy, :abort)
|
23
|
+
|
24
|
+
raise ArgumentError.new('idletime must be greater than zero') if idletime <= 0
|
25
|
+
raise ArgumentError.new('max_threads must be greater than zero') if max_length <= 0
|
26
|
+
raise ArgumentError.new("#{overflow_policy} is not a valid overflow policy") unless OVERFLOW_POLICIES.include?(overflow_policy)
|
27
|
+
|
28
|
+
opts = opts.merge(
|
29
|
+
min_threads: 0,
|
30
|
+
max_threads: max_length,
|
31
|
+
num_threads: overflow_policy,
|
32
|
+
idletime: idletime
|
33
|
+
)
|
34
|
+
super(opts)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'concurrent/ruby_thread_pool_executor'
|
2
|
+
|
3
|
+
module Concurrent
|
4
|
+
|
5
|
+
# @!macro fixed_thread_pool
|
6
|
+
class RubyFixedThreadPool < RubyThreadPoolExecutor
|
7
|
+
|
8
|
+
# Create a new thread pool.
|
9
|
+
#
|
10
|
+
# @param [Integer] num_threads the number of threads to allocate
|
11
|
+
# @param [Hash] opts the options defining pool behavior.
|
12
|
+
# @option opts [Symbol] :overflow_policy (+:abort+) the overflow policy
|
13
|
+
#
|
14
|
+
# @raise [ArgumentError] if +num_threads+ is less than or equal to zero
|
15
|
+
# @raise [ArgumentError] if +overflow_policy+ is not a known policy
|
16
|
+
def initialize(num_threads, opts = {})
|
17
|
+
overflow_policy = opts.fetch(:overflow_policy, :abort)
|
18
|
+
|
19
|
+
raise ArgumentError.new('number of threads must be greater than zero') if num_threads < 1
|
20
|
+
raise ArgumentError.new("#{overflow_policy} is not a valid overflow policy") unless OVERFLOW_POLICIES.include?(overflow_policy)
|
21
|
+
|
22
|
+
opts = opts.merge(
|
23
|
+
min_threads: num_threads,
|
24
|
+
max_threads: num_threads,
|
25
|
+
num_threads: overflow_policy,
|
26
|
+
idletime: 0
|
27
|
+
)
|
28
|
+
super(opts)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,268 @@
|
|
1
|
+
require 'thread'
|
2
|
+
|
3
|
+
require 'concurrent/event'
|
4
|
+
require 'concurrent/ruby_thread_pool_worker'
|
5
|
+
|
6
|
+
module Concurrent
|
7
|
+
|
8
|
+
RejectedExecutionError = Class.new(StandardError) unless defined? RejectedExecutionError
|
9
|
+
|
10
|
+
# @!macro thread_pool_executor
|
11
|
+
class RubyThreadPoolExecutor
|
12
|
+
|
13
|
+
# The maximum number of threads that will be created in the pool
|
14
|
+
# (unless overridden during construction).
|
15
|
+
DEFAULT_MAX_POOL_SIZE = 2**15 # 32768
|
16
|
+
|
17
|
+
# The minimum number of threads that will be created in the pool
|
18
|
+
# (unless overridden during construction).
|
19
|
+
DEFAULT_MIN_POOL_SIZE = 0
|
20
|
+
|
21
|
+
DEFAULT_MAX_QUEUE_SIZE = 0
|
22
|
+
|
23
|
+
# The maximum number of seconds a thread in the pool may remain idle before
|
24
|
+
# being reclaimed (unless overridden during construction).
|
25
|
+
DEFAULT_THREAD_IDLETIMEOUT = 60
|
26
|
+
|
27
|
+
OVERFLOW_POLICIES = [:abort, :discard, :caller_runs]
|
28
|
+
|
29
|
+
# The maximum number of threads that may be created in the pool.
|
30
|
+
attr_reader :max_length
|
31
|
+
attr_reader :min_length
|
32
|
+
|
33
|
+
attr_reader :largest_length
|
34
|
+
|
35
|
+
attr_reader :scheduled_task_count
|
36
|
+
attr_reader :completed_task_count
|
37
|
+
|
38
|
+
attr_reader :idletime
|
39
|
+
|
40
|
+
attr_reader :max_queue
|
41
|
+
|
42
|
+
attr_reader :overflow_policy
|
43
|
+
|
44
|
+
# Create a new thread pool.
|
45
|
+
#
|
46
|
+
# @see http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/ThreadPoolExecutor.html
|
47
|
+
def initialize(opts = {})
|
48
|
+
@min_length = opts.fetch(:min_threads, DEFAULT_MIN_POOL_SIZE).to_i
|
49
|
+
@max_length = opts.fetch(:max_threads, DEFAULT_MAX_POOL_SIZE).to_i
|
50
|
+
@idletime = opts.fetch(:idletime, DEFAULT_THREAD_IDLETIMEOUT).to_i
|
51
|
+
@max_queue = opts.fetch(:max_queue, DEFAULT_MAX_QUEUE_SIZE).to_i
|
52
|
+
@overflow_policy = opts.fetch(:overflow_policy, :abort)
|
53
|
+
|
54
|
+
raise ArgumentError.new('max_threads must be greater than zero') if @max_length <= 0
|
55
|
+
raise ArgumentError.new('min_threads cannot be less than zero') if @min_length < 0
|
56
|
+
raise ArgumentError.new("#{overflow_policy} is not a valid overflow policy") unless OVERFLOW_POLICIES.include?(@overflow_policy)
|
57
|
+
|
58
|
+
@state = :running
|
59
|
+
@pool = []
|
60
|
+
@terminator = Event.new
|
61
|
+
@queue = Queue.new
|
62
|
+
@mutex = Mutex.new
|
63
|
+
@scheduled_task_count = 0
|
64
|
+
@completed_task_count = 0
|
65
|
+
@largest_length = 0
|
66
|
+
|
67
|
+
@gc_interval = opts.fetch(:gc_interval, 1).to_i # undocumented
|
68
|
+
@last_gc_time = Time.now.to_f - [1.0, (@gc_interval * 2.0)].max
|
69
|
+
end
|
70
|
+
|
71
|
+
def length
|
72
|
+
@mutex.synchronize do
|
73
|
+
@state != :shutdown ? @pool.length : 0
|
74
|
+
end
|
75
|
+
end
|
76
|
+
alias_method :current_length, :length
|
77
|
+
|
78
|
+
def queue_length
|
79
|
+
@queue.length
|
80
|
+
end
|
81
|
+
|
82
|
+
def remaining_capacity
|
83
|
+
@mutex.synchronize { @max_queue == 0 ? -1 : @max_queue - @queue.length }
|
84
|
+
end
|
85
|
+
|
86
|
+
# Is the thread pool running?
|
87
|
+
#
|
88
|
+
# @return [Boolean] +true+ when running, +false+ when shutting down or shutdown
|
89
|
+
def running?
|
90
|
+
@mutex.synchronize { @state == :running }
|
91
|
+
end
|
92
|
+
|
93
|
+
# Returns an array with the status of each thread in the pool
|
94
|
+
#
|
95
|
+
# This method is deprecated and will be removed soon.
|
96
|
+
def status
|
97
|
+
warn '[DEPRECATED] `status` is deprecated and will be removed soon.'
|
98
|
+
@mutex.synchronize { @pool.collect { |worker| worker.status } }
|
99
|
+
end
|
100
|
+
|
101
|
+
# Is the thread pool shutdown?
|
102
|
+
#
|
103
|
+
# @return [Boolean] +true+ when shutdown, +false+ when shutting down or running
|
104
|
+
def shutdown?
|
105
|
+
@mutex.synchronize { @state != :running }
|
106
|
+
end
|
107
|
+
|
108
|
+
# Block until thread pool shutdown is complete or until +timeout+ seconds have
|
109
|
+
# passed.
|
110
|
+
#
|
111
|
+
# @note Does not initiate shutdown or termination. Either +shutdown+ or +kill+
|
112
|
+
# must be called before this method (or on another thread).
|
113
|
+
#
|
114
|
+
# @param [Integer] timeout the maximum number of seconds to wait for shutdown to complete
|
115
|
+
#
|
116
|
+
# @return [Boolean] +true+ if shutdown complete or false on +timeout+
|
117
|
+
def wait_for_termination(timeout)
|
118
|
+
return @terminator.wait(timeout.to_i)
|
119
|
+
end
|
120
|
+
|
121
|
+
# Submit a task to the thread pool for asynchronous processing.
|
122
|
+
#
|
123
|
+
# @param [Array] args zero or more arguments to be passed to the task
|
124
|
+
#
|
125
|
+
# @yield the asynchronous task to perform
|
126
|
+
#
|
127
|
+
# @return [Boolean] +true+ if the task is queued, +false+ if the thread pool
|
128
|
+
# is not running
|
129
|
+
#
|
130
|
+
# @raise [ArgumentError] if no task is given
|
131
|
+
def post(*args, &task)
|
132
|
+
raise ArgumentError.new('no block given') unless block_given?
|
133
|
+
@mutex.synchronize do
|
134
|
+
break false unless @state == :running
|
135
|
+
return handle_overflow(*args, &task) if @max_queue != 0 && @queue.length >= @max_queue
|
136
|
+
@scheduled_task_count += 1
|
137
|
+
@queue << [args, task]
|
138
|
+
if Time.now.to_f - @gc_interval >= @last_gc_time
|
139
|
+
prune_pool
|
140
|
+
@last_gc_time = Time.now.to_f
|
141
|
+
end
|
142
|
+
grow_pool
|
143
|
+
true
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
# Submit a task to the thread pool for asynchronous processing.
|
148
|
+
#
|
149
|
+
# @param [Proc] task the asynchronous task to perform
|
150
|
+
#
|
151
|
+
# @return [self] returns itself
|
152
|
+
def <<(task)
|
153
|
+
self.post(&task)
|
154
|
+
return self
|
155
|
+
end
|
156
|
+
|
157
|
+
# Begin an orderly shutdown. Tasks already in the queue will be executed,
|
158
|
+
# but no new tasks will be accepted. Has no additional effect if the
|
159
|
+
# thread pool is not running.
|
160
|
+
def shutdown
|
161
|
+
@mutex.synchronize do
|
162
|
+
break unless @state == :running
|
163
|
+
@queue.clear
|
164
|
+
if @pool.empty?
|
165
|
+
@state = :shutdown
|
166
|
+
@terminator.set
|
167
|
+
else
|
168
|
+
@state = :shuttingdown
|
169
|
+
@pool.length.times{ @queue << :stop }
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
# Begin an immediate shutdown. In-progress tasks will be allowed to
|
175
|
+
# complete but enqueued tasks will be dismissed and no new tasks
|
176
|
+
# will be accepted. Has no additional effect if the thread pool is
|
177
|
+
# not running.
|
178
|
+
def kill
|
179
|
+
@mutex.synchronize do
|
180
|
+
break if @state == :shutdown
|
181
|
+
@queue.clear
|
182
|
+
@state = :shutdown
|
183
|
+
drain_pool
|
184
|
+
@terminator.set
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
# @!visibility private
|
189
|
+
def on_end_task # :nodoc:
|
190
|
+
@mutex.synchronize do
|
191
|
+
@completed_task_count += 1 #if success
|
192
|
+
break unless @state == :running
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
# @!visibility private
|
197
|
+
def on_worker_exit(worker) # :nodoc:
|
198
|
+
@mutex.synchronize do
|
199
|
+
@pool.delete(worker)
|
200
|
+
if @pool.empty? && @state != :running
|
201
|
+
@state = :shutdown
|
202
|
+
@terminator.set
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
protected
|
208
|
+
|
209
|
+
# @!visibility private
|
210
|
+
def handle_overflow(*args) # :nodoc:
|
211
|
+
case @overflow_policy
|
212
|
+
when :abort
|
213
|
+
raise RejectedExecutionError
|
214
|
+
when :discard
|
215
|
+
false
|
216
|
+
when :caller_runs
|
217
|
+
begin
|
218
|
+
yield(*args)
|
219
|
+
rescue
|
220
|
+
# let it fail
|
221
|
+
end
|
222
|
+
true
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
# @!visibility private
|
227
|
+
def prune_pool # :nodoc:
|
228
|
+
@pool.delete_if do |worker|
|
229
|
+
worker.dead? ||
|
230
|
+
(@idletime == 0 ? false : Time.now.to_f - @idletime > worker.last_activity)
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
# @!visibility private
|
235
|
+
def grow_pool # :nodoc:
|
236
|
+
if @min_length > @pool.length
|
237
|
+
additional = @min_length - @pool.length
|
238
|
+
elsif @pool.length < @max_length && ! @queue.empty?
|
239
|
+
# NOTE: does not take into account idle threads
|
240
|
+
additional = 1
|
241
|
+
else
|
242
|
+
additional = 0
|
243
|
+
end
|
244
|
+
additional.times do
|
245
|
+
break if @pool.length >= @max_length
|
246
|
+
@pool << create_worker_thread
|
247
|
+
end
|
248
|
+
@largest_length = [@largest_length, @pool.length].max
|
249
|
+
end
|
250
|
+
|
251
|
+
# @!visibility private
|
252
|
+
def drain_pool # :nodoc:
|
253
|
+
@pool.each {|worker| worker.kill }
|
254
|
+
@pool.clear
|
255
|
+
end
|
256
|
+
|
257
|
+
# @!visibility private
|
258
|
+
def create_worker_thread # :nodoc:
|
259
|
+
wrkr = RubyThreadPoolWorker.new(@queue, self)
|
260
|
+
Thread.new(wrkr, self) do |worker, parent|
|
261
|
+
Thread.current.abort_on_exception = false
|
262
|
+
worker.run
|
263
|
+
parent.on_worker_exit(worker)
|
264
|
+
end
|
265
|
+
return wrkr
|
266
|
+
end
|
267
|
+
end
|
268
|
+
end
|