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.
@@ -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