proco 0.0.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.
- data/.gitignore +18 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +346 -0
- data/Rakefile +26 -0
- data/benchmark/comparison.rb +103 -0
- data/benchmark/submission.rb +150 -0
- data/lib/proco.rb +112 -0
- data/lib/proco/dispatcher.rb +59 -0
- data/lib/proco/future.rb +40 -0
- data/lib/proco/logger.rb +21 -0
- data/lib/proco/mt/base.rb +65 -0
- data/lib/proco/mt/pool.rb +53 -0
- data/lib/proco/mt/threaded.rb +52 -0
- data/lib/proco/mt/worker.rb +81 -0
- data/lib/proco/queue/base.rb +58 -0
- data/lib/proco/queue/batch_queue.rb +30 -0
- data/lib/proco/queue/multi_queue.rb +30 -0
- data/lib/proco/queue/single_queue.rb +23 -0
- data/lib/proco/version.rb +3 -0
- data/proco.gemspec +24 -0
- data/test/test_mt_base.rb +40 -0
- data/test/test_mt_threaded.rb +32 -0
- data/test/test_pool.rb +20 -0
- data/test/test_proco.rb +197 -0
- data/test/test_queue.rb +105 -0
- data/test/test_worker.rb +19 -0
- metadata +144 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
$VERBOSE = true
|
|
4
|
+
require 'rubygems'
|
|
5
|
+
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
|
|
6
|
+
require 'proco'
|
|
7
|
+
require 'benchmark'
|
|
8
|
+
require 'parallelize'
|
|
9
|
+
require 'thread'
|
|
10
|
+
require 'logger'
|
|
11
|
+
|
|
12
|
+
logger = Logger.new($stdout)
|
|
13
|
+
times = 1_000_000
|
|
14
|
+
|
|
15
|
+
if celluloid = false
|
|
16
|
+
require 'celluloid'
|
|
17
|
+
class Cell
|
|
18
|
+
include Celluloid
|
|
19
|
+
|
|
20
|
+
def push
|
|
21
|
+
Proco::Future.new
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
result = Benchmark.bm(40) do |x|
|
|
27
|
+
x.report('celluloid') do
|
|
28
|
+
c = Cell.new
|
|
29
|
+
f = nil
|
|
30
|
+
times.times do |i|
|
|
31
|
+
print '.' if i % 1000 == 0
|
|
32
|
+
c.push!
|
|
33
|
+
end
|
|
34
|
+
c.future.push.value
|
|
35
|
+
end if celluloid
|
|
36
|
+
|
|
37
|
+
x.report('simple loop') do
|
|
38
|
+
times.times do |i|
|
|
39
|
+
Proco::Future.new
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
x.report('Default Proco') do
|
|
44
|
+
proco = Proco.new
|
|
45
|
+
proco.start do |item|
|
|
46
|
+
nil
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
times.times do |i|
|
|
50
|
+
print '.' if i % 1000 == 0
|
|
51
|
+
proco.submit! i
|
|
52
|
+
end
|
|
53
|
+
proco.exit
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
x.report('SizedQueue push and pop') do
|
|
57
|
+
q = SizedQueue.new 1000
|
|
58
|
+
times.times do |i|
|
|
59
|
+
q.push Proco::Future.new
|
|
60
|
+
q.pop
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
x.report('SizedQueue push and pop (threads)') do
|
|
65
|
+
q = SizedQueue.new 1000
|
|
66
|
+
t1 = Thread.new do
|
|
67
|
+
times.times do |i|
|
|
68
|
+
q.push Proco::Future.new
|
|
69
|
+
end
|
|
70
|
+
q.push nil
|
|
71
|
+
end
|
|
72
|
+
t2 = Thread.new do
|
|
73
|
+
while q.pop
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
t1.join
|
|
78
|
+
t2.join
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
x.report('Mutex synchronization') do
|
|
82
|
+
m = Mutex.new
|
|
83
|
+
a = []
|
|
84
|
+
times.times do |i|
|
|
85
|
+
m.synchronize do
|
|
86
|
+
a << Proco::Future.new
|
|
87
|
+
end
|
|
88
|
+
m.synchronize do
|
|
89
|
+
a.shift
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
x.report('Proco queue') do
|
|
95
|
+
q = Proco::Queue::SingleQueue.new 1000
|
|
96
|
+
times.times do |i|
|
|
97
|
+
q.push i
|
|
98
|
+
q.take
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
x.report('Proco queue (thread)') do
|
|
103
|
+
q = Proco::Queue::SingleQueue.new 1000
|
|
104
|
+
t1 = Thread.new do
|
|
105
|
+
times.times do |i|
|
|
106
|
+
print '.' if i % 1000 == 0
|
|
107
|
+
q.push i
|
|
108
|
+
end
|
|
109
|
+
q.invalidate
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
t2 = Thread.new do
|
|
113
|
+
while true
|
|
114
|
+
f, i = q.take
|
|
115
|
+
break unless f
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
t1.join
|
|
120
|
+
t2.join
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
[1, 4, 16].each do |queues|
|
|
124
|
+
[1, 2, 4].each do |threads|
|
|
125
|
+
x.report("q: #{queues}, t: #{threads}") do
|
|
126
|
+
proco = Proco.queues(queues).logger(logger).threads(threads).new
|
|
127
|
+
proco.start do |items|
|
|
128
|
+
nil
|
|
129
|
+
end
|
|
130
|
+
threads = 1
|
|
131
|
+
parallelize(threads) do
|
|
132
|
+
(times / threads).times do |i|
|
|
133
|
+
print '.' if i % 1000 == 0
|
|
134
|
+
proco.submit! i
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
proco.kill
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
data = Hash[ result.map { |r| [r.label, r.real] } ]
|
|
144
|
+
mlen = data.keys.map(&:length).max
|
|
145
|
+
mval = data.values.max
|
|
146
|
+
width = 40
|
|
147
|
+
data.each do |k, v|
|
|
148
|
+
puts k.ljust(mlen) + ' : ' + '*' * (width * (v / mval)).to_i
|
|
149
|
+
end
|
|
150
|
+
|
data/lib/proco.rb
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
require 'proco/version'
|
|
2
|
+
require 'proco/logger'
|
|
3
|
+
require 'proco/mt/base'
|
|
4
|
+
require 'proco/mt/threaded'
|
|
5
|
+
require 'proco/mt/worker'
|
|
6
|
+
require 'proco/mt/pool'
|
|
7
|
+
require 'proco/dispatcher'
|
|
8
|
+
require 'proco/future'
|
|
9
|
+
require 'proco/queue/base'
|
|
10
|
+
require 'proco/queue/single_queue'
|
|
11
|
+
require 'proco/queue/batch_queue'
|
|
12
|
+
require 'proco/queue/multi_queue'
|
|
13
|
+
require 'option_initializer'
|
|
14
|
+
|
|
15
|
+
class Array
|
|
16
|
+
def sample
|
|
17
|
+
self[Kernel.rand(size)]
|
|
18
|
+
end unless method_defined? :sample
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
class Proco
|
|
22
|
+
include Proco::Logger
|
|
23
|
+
include OptionInitializer
|
|
24
|
+
|
|
25
|
+
option_initializer :interval, :threads, :queues, :queue_size, :batch, :batch_size, :logger
|
|
26
|
+
option_validator do |opt, val|
|
|
27
|
+
case opt
|
|
28
|
+
when :interval
|
|
29
|
+
raise ArgumentError, "interval must be a number" unless val.is_a?(Numeric)
|
|
30
|
+
when :threads, :queues, :queue_size, :batch_size
|
|
31
|
+
raise ArgumentError, "#{opt} must be a positive non-zero integer" unless val.is_a?(Fixnum) && val > 0
|
|
32
|
+
when :batch
|
|
33
|
+
raise ArgumentError, "batch must be a boolean value" unless [true, false].include?(val)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
attr_reader :options
|
|
38
|
+
|
|
39
|
+
DEFAULT_OPTIONS = {
|
|
40
|
+
:interval => 0,
|
|
41
|
+
:threads => 1,
|
|
42
|
+
:queues => 1,
|
|
43
|
+
:queue_size => 1000,
|
|
44
|
+
:batch => false
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
def initialize options = {}, &processor
|
|
48
|
+
validate_options options
|
|
49
|
+
@options = DEFAULT_OPTIONS.merge(options)
|
|
50
|
+
@logger = @options[:logger]
|
|
51
|
+
|
|
52
|
+
@pool = nil
|
|
53
|
+
@running = false
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# @return [Proco]
|
|
57
|
+
def start &block
|
|
58
|
+
raise ArgumentError, "Block not given" if block.nil?
|
|
59
|
+
@running = true
|
|
60
|
+
@pool = Proco::MT::Pool.new(options[:threads], @logger)
|
|
61
|
+
@dispatchers = options[:queues].times.map { |i|
|
|
62
|
+
Dispatcher.new(self, @pool, block)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
self
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Synchronous submission
|
|
69
|
+
# @param [Object] items
|
|
70
|
+
# @return [Hash]
|
|
71
|
+
def submit item
|
|
72
|
+
check_running
|
|
73
|
+
submit!(item).get
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Asynchronous submission
|
|
77
|
+
# @param [Object] items
|
|
78
|
+
# @return [Proco::Future]
|
|
79
|
+
def submit! item
|
|
80
|
+
check_running
|
|
81
|
+
@dispatchers.sample.push(item)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Stops Proco, returns results from remaining submissions in the queue.
|
|
85
|
+
# @return [nil]
|
|
86
|
+
def exit
|
|
87
|
+
check_running
|
|
88
|
+
@running = false
|
|
89
|
+
@dispatchers.each(&:exit)
|
|
90
|
+
@pool.exit
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# @return [nil]
|
|
94
|
+
def kill
|
|
95
|
+
check_running
|
|
96
|
+
@running = false
|
|
97
|
+
@dispatchers.each(&:kill)
|
|
98
|
+
@pool.kill
|
|
99
|
+
nil
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# @return [Boolean]
|
|
103
|
+
def running?
|
|
104
|
+
@running
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
private
|
|
108
|
+
def check_running
|
|
109
|
+
raise RuntimeError, "Not running" unless running?
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
require 'lps'
|
|
2
|
+
|
|
3
|
+
class Proco
|
|
4
|
+
# @private
|
|
5
|
+
class Dispatcher
|
|
6
|
+
include Proco::MT::Threaded
|
|
7
|
+
|
|
8
|
+
def initialize proco, thread_pool, block
|
|
9
|
+
super()
|
|
10
|
+
|
|
11
|
+
@logger, interval, qs, batch, batch_size =
|
|
12
|
+
proco.options.values_at :logger, :interval, :queue_size, :batch, :batch_size
|
|
13
|
+
@queue = if batch && batch_size
|
|
14
|
+
Proco::Queue::BatchQueue.new(qs, batch_size)
|
|
15
|
+
elsif batch
|
|
16
|
+
Proco::Queue::MultiQueue.new(qs)
|
|
17
|
+
else
|
|
18
|
+
Proco::Queue::SingleQueue.new(qs)
|
|
19
|
+
end
|
|
20
|
+
@pool = thread_pool
|
|
21
|
+
@block = block
|
|
22
|
+
|
|
23
|
+
spawn do
|
|
24
|
+
future = items = nil
|
|
25
|
+
LPS.interval(interval).while {
|
|
26
|
+
future, items = @queue.take
|
|
27
|
+
future # JRuby bug
|
|
28
|
+
}.loop do
|
|
29
|
+
inner_loop future, items
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def push *items
|
|
35
|
+
@queue.push(*items)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def exit
|
|
39
|
+
@queue.invalidate
|
|
40
|
+
super
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
def inner_loop future, items
|
|
45
|
+
@pool.assign do
|
|
46
|
+
future.update do
|
|
47
|
+
begin
|
|
48
|
+
@block.call items
|
|
49
|
+
rescue Exception => e
|
|
50
|
+
error e.to_s # TODO
|
|
51
|
+
raise
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
future
|
|
57
|
+
end
|
|
58
|
+
end#Dispatcher
|
|
59
|
+
end#Proco
|
data/lib/proco/future.rb
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
class Proco
|
|
2
|
+
class Future
|
|
3
|
+
include Proco::MT::Base
|
|
4
|
+
|
|
5
|
+
def get
|
|
6
|
+
do_when(proc { @state != :wait }) do
|
|
7
|
+
if @state == :ok
|
|
8
|
+
return @return
|
|
9
|
+
else
|
|
10
|
+
raise @return
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def inspect
|
|
16
|
+
"Future=#{@state}"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# @private
|
|
20
|
+
def initialize
|
|
21
|
+
super()
|
|
22
|
+
@state = :wait
|
|
23
|
+
@return = nil
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @private
|
|
27
|
+
def update
|
|
28
|
+
begin
|
|
29
|
+
@return = yield
|
|
30
|
+
@state = :ok
|
|
31
|
+
rescue Exception => e
|
|
32
|
+
@return = e
|
|
33
|
+
@state = :fail
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
broadcast
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
data/lib/proco/logger.rb
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
require 'forwardable'
|
|
2
|
+
|
|
3
|
+
class Proco
|
|
4
|
+
module Logger
|
|
5
|
+
extend Forwardable
|
|
6
|
+
|
|
7
|
+
class DummyLogger
|
|
8
|
+
[:info, :debug, :warn, :error].each do |m|
|
|
9
|
+
define_method m do |msg|
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def logger
|
|
15
|
+
@logger ||= DummyLogger.new
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def_delegators :logger, :info, :debug, :warn, :error
|
|
19
|
+
end#Logger
|
|
20
|
+
end#Proco
|
|
21
|
+
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
class Proco
|
|
2
|
+
module MT
|
|
3
|
+
# @private
|
|
4
|
+
module Base
|
|
5
|
+
def initialize
|
|
6
|
+
@mtx = Mutex.new
|
|
7
|
+
@cv = ConditionVariable.new
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def try_when cond, &block
|
|
11
|
+
return unless @mtx.try_lock
|
|
12
|
+
|
|
13
|
+
begin
|
|
14
|
+
block.call if cond.call
|
|
15
|
+
ensure
|
|
16
|
+
@cv.broadcast
|
|
17
|
+
@mtx.unlock
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def do_when cond, &block
|
|
22
|
+
@mtx.lock
|
|
23
|
+
while !cond.call
|
|
24
|
+
@cv.wait @mtx
|
|
25
|
+
end
|
|
26
|
+
block.call
|
|
27
|
+
ensure
|
|
28
|
+
# A good discussion on the use of broadcast instead of signal
|
|
29
|
+
# http://stackoverflow.com/questions/37026/java-notify-vs-notifyall-all-over-again
|
|
30
|
+
@cv.broadcast
|
|
31
|
+
@mtx.unlock
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def synchronize
|
|
35
|
+
@mtx.synchronize do
|
|
36
|
+
yield
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def wait_until &cond
|
|
41
|
+
do_when(cond) {}
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def signal &block
|
|
45
|
+
@mtx.synchronize do
|
|
46
|
+
begin
|
|
47
|
+
block.call if block
|
|
48
|
+
ensure
|
|
49
|
+
@cv.signal
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def broadcast &block
|
|
55
|
+
@mtx.synchronize do
|
|
56
|
+
begin
|
|
57
|
+
block.call if block
|
|
58
|
+
ensure
|
|
59
|
+
@cv.broadcast
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end#Base
|
|
64
|
+
end#MT
|
|
65
|
+
end
|