in_threads 1.0.0 → 1.1.0
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/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
|