in_threads 1.0.0 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/in_threads.gemspec +1 -1
- data/lib/in_threads.rb +124 -58
- data/spec/in_threads_spec.rb +148 -13
- metadata +4 -4
data/in_threads.gemspec
CHANGED
data/lib/in_threads.rb
CHANGED
@@ -3,63 +3,112 @@ require 'thwait'
|
|
3
3
|
|
4
4
|
module Enumerable
|
5
5
|
# Run enumerable method blocks in threads
|
6
|
+
#
|
7
|
+
# urls.in_threads.map do |url|
|
8
|
+
# url.fetch
|
9
|
+
# end
|
10
|
+
#
|
11
|
+
# Specify number of threads to use:
|
12
|
+
#
|
13
|
+
# files.in_threads(4).all? do |file|
|
14
|
+
# file.valid?
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# Passing block runs it against <tt>each</tt>
|
18
|
+
#
|
19
|
+
# urls.in_threads.each{ … }
|
20
|
+
#
|
21
|
+
# is same as
|
22
|
+
#
|
23
|
+
# urls.in_threads{ … }
|
6
24
|
def in_threads(thread_count = 10, &block)
|
7
25
|
InThreads.new(self, thread_count, &block)
|
8
26
|
end
|
9
27
|
end
|
10
28
|
|
11
|
-
# TODO: create my own ThreadsWait with blackjack and hookers?
|
12
|
-
# TODO: create class methods for connecting Enumerable method to runner
|
13
|
-
# TODO: run_in_threads_inconsecutive for `all?`, `any?`, `none?` and `one?`
|
14
29
|
# TODO: all ruby1.9.3 methods
|
15
|
-
# TODO: better way of handling grep?
|
16
|
-
# TODO: check method presence if Enumerable before connectin to runner
|
17
30
|
|
18
31
|
class InThreads
|
19
32
|
(
|
20
33
|
instance_methods.map(&:to_s) -
|
21
34
|
%w[__id__ __send__ class inspect instance_of? is_a? kind_of? nil? object_id respond_to? send]
|
22
35
|
).each{ |name| undef_method name }
|
23
|
-
(private_instance_methods.map(&:to_s) - %w[initialize]).each{ |name| undef_method name }
|
36
|
+
(private_instance_methods.map(&:to_s) - %w[initialize raise]).each{ |name| undef_method name }
|
24
37
|
|
25
38
|
attr_reader :enumerable, :thread_count
|
26
39
|
def initialize(enumerable, thread_count = 10, &block)
|
27
|
-
@enumerable, @thread_count = enumerable, thread_count
|
40
|
+
@enumerable, @thread_count = enumerable, thread_count.to_i
|
41
|
+
unless enumerable.class.include?(Enumerable)
|
42
|
+
raise ArgumentError.new('`enumerable` should include Enumerable.')
|
43
|
+
end
|
44
|
+
if thread_count < 2
|
45
|
+
raise ArgumentError.new('`thread_count` can\'t be less than 2.')
|
46
|
+
end
|
28
47
|
each(&block) if block
|
29
48
|
end
|
30
49
|
|
50
|
+
# Creates new instance using underlying enumerable and new thread_count
|
31
51
|
def in_threads(thread_count = 10, &block)
|
32
52
|
self.class.new(enumerable, thread_count, &block)
|
33
53
|
end
|
34
54
|
|
35
|
-
|
36
|
-
|
55
|
+
class << self
|
56
|
+
# List of instance_methods of Enumerable
|
57
|
+
def enumerable_methods
|
58
|
+
Enumerable.instance_methods.map(&:to_s)
|
59
|
+
end
|
60
|
+
|
61
|
+
# Specify runner to use
|
62
|
+
#
|
63
|
+
# use :run_in_threads_consecutive, :for => %w[all? any? none? one?]
|
64
|
+
#
|
65
|
+
# <tt>:for</tt> is required
|
66
|
+
# <tt>:ignore_undefined</tt> ignores methods which are not present in list returned by <tt>enumerable_methods</tt>
|
67
|
+
def use(runner, options)
|
68
|
+
methods = Array(options[:for])
|
69
|
+
raise 'no methods provided using :for option' if methods.empty?
|
70
|
+
ignore_undefined = options[:ignore_undefined]
|
71
|
+
enumerable_methods = self.enumerable_methods
|
72
|
+
methods.each do |method|
|
73
|
+
unless ignore_undefined && !enumerable_methods.include?(method)
|
74
|
+
class_eval <<-RUBY
|
75
|
+
def #{method}(*args, &block)
|
76
|
+
#{runner}(enumerable, :#{method}, *args, &block)
|
77
|
+
end
|
78
|
+
RUBY
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
use :run_in_threads_block_result_irrelevant, :for => %w[each]
|
85
|
+
use :run_in_threads_consecutive, :for => %w[
|
37
86
|
all? any? none? one?
|
38
87
|
detect find find_index drop_while take_while
|
39
88
|
partition find_all select reject count
|
40
89
|
collect map group_by max_by min_by minmax_by sort_by
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
run_in_threads_consecutive(enumerable, :#{name}, *args, &block)
|
45
|
-
end
|
46
|
-
RUBY
|
47
|
-
end
|
48
|
-
|
49
|
-
%w[
|
90
|
+
flat_map collect_concat
|
91
|
+
], :ignore_undefined => true
|
92
|
+
use :run_in_threads_block_result_irrelevant, :for => %w[
|
50
93
|
reverse_each
|
51
94
|
each_with_index enum_with_index
|
52
95
|
each_cons each_slice enum_cons enum_slice
|
53
96
|
zip
|
54
97
|
cycle
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
98
|
+
each_entry
|
99
|
+
], :ignore_undefined => true
|
100
|
+
use :run_without_threads, :for => %w[
|
101
|
+
inject reduce
|
102
|
+
max min minmax sort
|
103
|
+
entries to_a
|
104
|
+
drop take
|
105
|
+
first
|
106
|
+
include? member?
|
107
|
+
each_with_object
|
108
|
+
chunk slice_before
|
109
|
+
], :ignore_undefined => true
|
62
110
|
|
111
|
+
# Special case method, works by applying <tt>run_in_threads_consecutive</tt> with map on enumerable returned by blockless run
|
63
112
|
def grep(*args, &block)
|
64
113
|
if block
|
65
114
|
run_in_threads_consecutive(enumerable.grep(*args), :map, &block)
|
@@ -68,63 +117,75 @@ class InThreads
|
|
68
117
|
end
|
69
118
|
end
|
70
119
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
120
|
+
protected
|
121
|
+
|
122
|
+
# Use ThreadsWait to limit number of threads
|
123
|
+
class ThreadLimiter
|
124
|
+
# Initialize with limit
|
125
|
+
def initialize(count)
|
126
|
+
@count = count
|
127
|
+
@waiter = ThreadsWait.new
|
128
|
+
end
|
129
|
+
|
130
|
+
# Without block behaves as <tt>new</tt>
|
131
|
+
# With block yields it with <tt>self</tt> and ensures running of <tt>finalize</tt>
|
132
|
+
def self.limit(count, &block)
|
133
|
+
limiter = new(count)
|
134
|
+
if block
|
135
|
+
begin
|
136
|
+
yield limiter
|
137
|
+
ensure
|
138
|
+
limiter.finalize
|
139
|
+
end
|
140
|
+
else
|
141
|
+
limiter
|
82
142
|
end
|
83
|
-
|
84
|
-
end
|
143
|
+
end
|
85
144
|
|
86
|
-
|
145
|
+
# Add thread to <tt>ThreadsWait</tt>, wait for finishing of one thread if limit reached
|
146
|
+
def add(thread)
|
147
|
+
if @waiter.threads.length + 1 >= @count
|
148
|
+
@waiter.join(thread)
|
149
|
+
else
|
150
|
+
@waiter.join_nowait(thread)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
# Wait for waiting threads
|
155
|
+
def finalize
|
156
|
+
@waiter.all_waits
|
157
|
+
end
|
158
|
+
end
|
87
159
|
|
160
|
+
# Use for methods which don't use block result
|
88
161
|
def run_in_threads_block_result_irrelevant(enumerable, method, *args, &block)
|
89
162
|
if block
|
90
|
-
|
91
|
-
begin
|
163
|
+
ThreadLimiter.limit(thread_count) do |limiter|
|
92
164
|
enumerable.send(method, *args) do |*block_args|
|
93
|
-
|
94
|
-
waiter.join_nowait([Thread.new(*block_args, &block)])
|
165
|
+
limiter.add(Thread.new(*block_args, &block))
|
95
166
|
end
|
96
|
-
ensure
|
97
|
-
waiter.all_waits
|
98
167
|
end
|
99
168
|
else
|
100
169
|
enumerable.send(method, *args)
|
101
170
|
end
|
102
171
|
end
|
103
172
|
|
173
|
+
# Use for methods which do use block result and fire objects in same way as <tt>each</tt>
|
104
174
|
def run_in_threads_consecutive(enumerable, method, *args, &block)
|
105
175
|
if block
|
106
176
|
begin
|
107
177
|
queue = Queue.new
|
108
|
-
runner = Thread.new
|
109
|
-
|
110
|
-
begin
|
178
|
+
runner = Thread.new do
|
179
|
+
ThreadLimiter.limit(thread_count) do |limiter|
|
111
180
|
enumerable.each do |object|
|
112
|
-
if threads.length >= thread_count
|
113
|
-
threads = threads.select(&:alive?)
|
114
|
-
if threads.length >= thread_count
|
115
|
-
ThreadsWait.new(*threads).next_wait
|
116
|
-
end
|
117
|
-
end
|
118
181
|
break if Thread.current[:stop]
|
119
182
|
thread = Thread.new(object, &block)
|
120
|
-
threads << thread
|
121
183
|
queue << thread
|
184
|
+
limiter.add(thread)
|
122
185
|
end
|
123
|
-
ensure
|
124
|
-
threads.map(&:join)
|
125
186
|
end
|
126
187
|
end
|
127
|
-
enumerable.send(method, *args) do
|
188
|
+
enumerable.send(method, *args) do |object|
|
128
189
|
queue.pop.value
|
129
190
|
end
|
130
191
|
ensure
|
@@ -135,4 +196,9 @@ private
|
|
135
196
|
enumerable.send(method, *args)
|
136
197
|
end
|
137
198
|
end
|
199
|
+
|
200
|
+
# Use for methods which don't use blocks or can not use threads
|
201
|
+
def run_without_threads(enumerable, method, *args, &block)
|
202
|
+
enumerable.send(method, *args, &block)
|
203
|
+
end
|
138
204
|
end
|
data/spec/in_threads_spec.rb
CHANGED
@@ -48,8 +48,16 @@ class ValueItem < Item
|
|
48
48
|
end
|
49
49
|
end
|
50
50
|
|
51
|
-
def
|
52
|
-
|
51
|
+
def describe_enum_method(method, &block)
|
52
|
+
@enum_methods ||= Enumerable.instance_methods.map(&:to_s)
|
53
|
+
if @enum_methods.include?(method)
|
54
|
+
describe(method, &block)
|
55
|
+
else
|
56
|
+
it "should not be defined" do
|
57
|
+
exception_regexp = /^undefined method `#{Regexp.escape(method)}' for #<InThreads:0x[0-9a-f]+>$/
|
58
|
+
proc{ enum.in_threads.send(method) }.should raise_error(NoMethodError, exception_regexp)
|
59
|
+
end
|
60
|
+
end
|
53
61
|
end
|
54
62
|
|
55
63
|
describe "in_threads" do
|
@@ -62,6 +70,30 @@ describe "in_threads" do
|
|
62
70
|
Time.now - start
|
63
71
|
end
|
64
72
|
|
73
|
+
(Enumerable.instance_methods - 10.times.in_threads.class.instance_methods).each do |method|
|
74
|
+
pending method
|
75
|
+
end
|
76
|
+
|
77
|
+
describe "verifying params" do
|
78
|
+
it "should complain about using with non enumerable" do
|
79
|
+
proc{ InThreads.new(1) }.should raise_error(ArgumentError)
|
80
|
+
end
|
81
|
+
|
82
|
+
[1..10, 10.times, {}, []].each do |o|
|
83
|
+
it "should complain about using with #{o.class}" do
|
84
|
+
proc{ InThreads.new(o) }.should_not raise_error
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
it "should complain about using less than 2 threads" do
|
89
|
+
proc{ 10.times.in_threads(1) }.should raise_error(ArgumentError)
|
90
|
+
end
|
91
|
+
|
92
|
+
it "should not complain about using 2 or more threads" do
|
93
|
+
proc{ 10.times.in_threads(2) }.should_not raise_error
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
65
97
|
describe "in_threads" do
|
66
98
|
it "should not change existing instance" do
|
67
99
|
threaded = enum.in_threads(10)
|
@@ -79,6 +111,31 @@ describe "in_threads" do
|
|
79
111
|
end
|
80
112
|
end
|
81
113
|
|
114
|
+
describe "thread count" do
|
115
|
+
let(:enum){ 100.times.map{ |i| ValueItem.new(i, i < 50) } }
|
116
|
+
|
117
|
+
%w[each map all?].each do |method|
|
118
|
+
it "should run in specified number of threads for #{method}" do
|
119
|
+
@thread_count = 0
|
120
|
+
@max_thread_count = 0
|
121
|
+
@mutex = Mutex.new
|
122
|
+
enum.in_threads(13).send(method) do |o|
|
123
|
+
@mutex.synchronize do
|
124
|
+
@thread_count += 1
|
125
|
+
@max_thread_count = [@max_thread_count, @thread_count].max
|
126
|
+
end
|
127
|
+
res = o.check?
|
128
|
+
@mutex.synchronize do
|
129
|
+
@thread_count -= 1
|
130
|
+
end
|
131
|
+
res
|
132
|
+
end
|
133
|
+
@thread_count.should == 0
|
134
|
+
@max_thread_count.should == 13
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
82
139
|
describe "each" do
|
83
140
|
it "should return same enum after running" do
|
84
141
|
enum.in_threads.each(&:value).should == enum
|
@@ -86,7 +143,7 @@ describe "in_threads" do
|
|
86
143
|
|
87
144
|
it "should execute block for each element" do
|
88
145
|
enum.each{ |o| o.should_receive(:touch).once }
|
89
|
-
enum.in_threads.each(&:
|
146
|
+
enum.in_threads.each(&:touch_n_value)
|
90
147
|
end
|
91
148
|
|
92
149
|
it "should run faster with threads" do
|
@@ -94,7 +151,7 @@ describe "in_threads" do
|
|
94
151
|
end
|
95
152
|
|
96
153
|
it "should run faster with more threads" do
|
97
|
-
measure{ enum.in_threads(
|
154
|
+
measure{ enum.in_threads(10).each(&:work) }.should < measure{ enum.in_threads(2).each(&:work) } * speed_coef
|
98
155
|
end
|
99
156
|
|
100
157
|
it "should return same enum without block" do
|
@@ -102,8 +159,8 @@ describe "in_threads" do
|
|
102
159
|
end
|
103
160
|
end
|
104
161
|
|
105
|
-
|
106
|
-
|
162
|
+
%w[each_with_index enum_with_index].each do |method|
|
163
|
+
describe_enum_method method do
|
107
164
|
let(:runner){ proc{ |o, i| o.value } }
|
108
165
|
|
109
166
|
it "should return same result with threads" do
|
@@ -131,14 +188,14 @@ describe "in_threads" do
|
|
131
188
|
end
|
132
189
|
|
133
190
|
it "should fire same objects in reverse order" do
|
134
|
-
@order = mock('order', :
|
135
|
-
@order.should_receive(:
|
136
|
-
@order.should_receive(:
|
137
|
-
@order.should_receive(:
|
191
|
+
@order = mock('order', :notify => nil)
|
192
|
+
@order.should_receive(:notify).with(enum.last).ordered
|
193
|
+
@order.should_receive(:notify).with(enum[enum.length / 2]).ordered
|
194
|
+
@order.should_receive(:notify).with(enum.first).ordered
|
138
195
|
enum.reverse_each{ |o| o.should_receive(:touch).once }
|
139
196
|
@mutex = Mutex.new
|
140
197
|
enum.in_threads.reverse_each do |o|
|
141
|
-
@mutex.synchronize{ @order.
|
198
|
+
@mutex.synchronize{ @order.notify(o) }
|
142
199
|
o.touch_n_value
|
143
200
|
end
|
144
201
|
end
|
@@ -220,8 +277,8 @@ describe "in_threads" do
|
|
220
277
|
end
|
221
278
|
end
|
222
279
|
|
223
|
-
|
224
|
-
|
280
|
+
%w[each_cons each_slice enum_slice enum_cons].each do |method|
|
281
|
+
describe_enum_method method do
|
225
282
|
let(:runner){ proc{ |a| a.each(&:value) } }
|
226
283
|
|
227
284
|
it "should fire same objects" do
|
@@ -300,6 +357,68 @@ describe "in_threads" do
|
|
300
357
|
end
|
301
358
|
end
|
302
359
|
|
360
|
+
describe_enum_method "each_entry" do
|
361
|
+
class EachEntryYielder
|
362
|
+
include Enumerable
|
363
|
+
def each
|
364
|
+
10.times{ yield 1 }
|
365
|
+
10.times{ yield 2, 3 }
|
366
|
+
10.times{ yield }
|
367
|
+
end
|
368
|
+
end
|
369
|
+
|
370
|
+
let(:enum){ EachEntryYielder.new }
|
371
|
+
let(:runner){ proc{ |o| ValueItem.new(0, o).work } }
|
372
|
+
|
373
|
+
it "should return same result with threads" do
|
374
|
+
enum.in_threads.each_entry(&runner).should == enum.each_entry(&runner)
|
375
|
+
end
|
376
|
+
|
377
|
+
it "should execute block for each element" do
|
378
|
+
@order = mock('order')
|
379
|
+
@order.should_receive(:notify).with(1).exactly(10).times.ordered
|
380
|
+
@order.should_receive(:notify).with([2, 3]).exactly(10).times.ordered
|
381
|
+
@order.should_receive(:notify).with(nil).exactly(10).times.ordered
|
382
|
+
@mutex = Mutex.new
|
383
|
+
enum.in_threads.each_entry do |o|
|
384
|
+
@mutex.synchronize{ @order.notify(o) }
|
385
|
+
runner[]
|
386
|
+
end
|
387
|
+
end
|
388
|
+
|
389
|
+
it "should run faster with threads" do
|
390
|
+
measure{ enum.in_threads.each_entry(&runner) }.should < measure{ enum.each_entry(&runner) } * speed_coef
|
391
|
+
end
|
392
|
+
|
393
|
+
it "should return same enum without block" do
|
394
|
+
enum.in_threads.each_entry.to_a.should == enum.each_entry.to_a
|
395
|
+
end
|
396
|
+
end
|
397
|
+
|
398
|
+
%w[flat_map collect_concat].each do |method|
|
399
|
+
describe_enum_method method do
|
400
|
+
let(:enum){ 20.times.map{ |i| Item.new(i) }.each_slice(3) }
|
401
|
+
let(:runner){ proc{ |a| a.map(&:value) } }
|
402
|
+
|
403
|
+
it "should return same result with threads" do
|
404
|
+
enum.in_threads.send(method, &runner).should == enum.send(method, &runner)
|
405
|
+
end
|
406
|
+
|
407
|
+
it "should fire same objects" do
|
408
|
+
enum.send(method){ |a| a.each{ |o| o.should_receive(:touch).with(a).once } }
|
409
|
+
enum.in_threads.send(method){ |a| a.each{ |o| o.touch_n_value(a) } }
|
410
|
+
end
|
411
|
+
|
412
|
+
it "should run faster with threads" do
|
413
|
+
measure{ enum.in_threads.send(method, &runner) }.should < measure{ enum.send(method, &runner) } * speed_coef
|
414
|
+
end
|
415
|
+
|
416
|
+
it "should return same enum without block" do
|
417
|
+
enum.in_threads.send(method).to_a.should == enum.send(method).to_a
|
418
|
+
end
|
419
|
+
end
|
420
|
+
end
|
421
|
+
|
303
422
|
context "unthreaded" do
|
304
423
|
%w[inject reduce].each do |method|
|
305
424
|
describe method do
|
@@ -351,5 +470,21 @@ describe "in_threads" do
|
|
351
470
|
end
|
352
471
|
end
|
353
472
|
end
|
473
|
+
|
474
|
+
describe_enum_method "each_with_object" do
|
475
|
+
let(:runner){ proc{ |o, h| h[o.value] = true } }
|
476
|
+
|
477
|
+
it "should return same result" do
|
478
|
+
enum.in_threads.each_with_object({}, &runner).should == enum.each_with_object({}, &runner)
|
479
|
+
end
|
480
|
+
end
|
481
|
+
|
482
|
+
%w[chunk slice_before].each do |method|
|
483
|
+
describe_enum_method method do
|
484
|
+
it "should return same result" do
|
485
|
+
enum.in_threads.send(method, &:check?).to_a.should == enum.send(method, &:check?).to_a
|
486
|
+
end
|
487
|
+
end
|
488
|
+
end
|
354
489
|
end
|
355
490
|
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: in_threads
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 19
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 1
|
8
|
+
- 1
|
8
9
|
- 0
|
9
|
-
|
10
|
-
version: 1.0.0
|
10
|
+
version: 1.1.0
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Ivan Kuchin
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2011-12-
|
18
|
+
date: 2011-12-08 00:00:00 Z
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
21
21
|
name: rspec
|