concurrent-ruby 0.5.0 → 0.6.0.pre.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|