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.
@@ -2,7 +2,7 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = 'in_threads'
5
- s.version = '1.0.0'
5
+ s.version = '1.1.0'
6
6
  s.summary = %q{Execute ruby code in parallel}
7
7
  s.homepage = "http://github.com/toy/#{s.name}"
8
8
  s.authors = ['Ivan Kuchin']
@@ -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
- %w[
36
- each
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
- ].each do |name|
42
- class_eval <<-RUBY
43
- def #{name}(*args, &block)
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
- ].each do |name|
56
- class_eval <<-RUBY
57
- def #{name}(*args, &block)
58
- run_in_threads_block_result_irrelevant(enumerable, :#{name}, *args, &block)
59
- end
60
- RUBY
61
- end
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
- %w[
72
- inject reduce
73
- max min minmax sort
74
- entries to_a
75
- drop take
76
- first
77
- include? member?
78
- ].each do |name|
79
- class_eval <<-RUBY
80
- def #{name}(*args, &block)
81
- enumerable.#{name}(*args, &block)
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
- RUBY
84
- end
143
+ end
85
144
 
86
- private
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
- waiter = ThreadsWait.new
91
- begin
163
+ ThreadLimiter.limit(thread_count) do |limiter|
92
164
  enumerable.send(method, *args) do |*block_args|
93
- waiter.next_wait if waiter.threads.length >= thread_count
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(enumerable) do |enumerable|
109
- threads = []
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 |*block_args|
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
@@ -48,8 +48,16 @@ class ValueItem < Item
48
48
  end
49
49
  end
50
50
 
51
- def enum_methods(methods)
52
- (Enumerable.instance_methods.map(&:to_s) & methods)
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(&:touch)
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(20).each(&:work) }.should < measure{ enum.in_threads(2).each(&:work) } * speed_coef
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
- enum_methods(%w[each_with_index enum_with_index]).each do |method|
106
- describe method do
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', :touch => nil)
135
- @order.should_receive(:touch).with(enum.last).ordered
136
- @order.should_receive(:touch).with(enum[enum.length / 2]).ordered
137
- @order.should_receive(:touch).with(enum.first).ordered
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.touch(o) }
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
- enum_methods(%w[each_cons each_slice enum_slice enum_cons]).each do |method|
224
- describe method do
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: 23
4
+ hash: 19
5
5
  prerelease:
6
6
  segments:
7
7
  - 1
8
+ - 1
8
9
  - 0
9
- - 0
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-05 00:00:00 Z
18
+ date: 2011-12-08 00:00:00 Z
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
21
21
  name: rspec