parallel 0.4.6 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source 'http://rubygems.org'
2
+
3
+ gem 'rake'
4
+ gem 'rspec', '~>1'
5
+ gem 'jeweler'
data/Gemfile.lock ADDED
@@ -0,0 +1,22 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ gemcutter (0.6.1)
5
+ git (1.2.5)
6
+ jeweler (1.4.0)
7
+ gemcutter (>= 0.1.0)
8
+ git (>= 1.2.5)
9
+ rubyforge (>= 2.0.0)
10
+ json_pure (1.4.6)
11
+ rake (0.8.7)
12
+ rspec (1.3.1)
13
+ rubyforge (2.0.4)
14
+ json_pure (>= 1.1.7)
15
+
16
+ PLATFORMS
17
+ ruby
18
+
19
+ DEPENDENCIES
20
+ jeweler
21
+ rake
22
+ rspec (~> 1)
data/Readme.md CHANGED
@@ -51,6 +51,7 @@ Authors
51
51
  - [Fred Wu](http://fredwu.me)
52
52
  - [mikezter](http://github.com/mikezter)
53
53
  - [Jeremy Durham](http://www.jeremydurham.com)
54
+ - [Nick Gauthier](http://www.ngauthier.com)
54
55
 
55
56
  [Michael Grosser](http://pragmatig.wordpress.com)
56
57
  grosser.michael@gmail.com
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.4.6
1
+ 0.5.0
data/lib/parallel.rb CHANGED
@@ -1,8 +1,8 @@
1
1
  require 'thread' # to get Thread.exclusive
2
+ require 'base64'
2
3
 
3
4
  class Parallel
4
5
  VERSION = File.read( File.join(File.dirname(__FILE__),'..','VERSION') ).strip
5
- SPLAT_BUG = *[] # fix for bug/feature http://redmine.ruby-lang.org/issues/show/2422
6
6
 
7
7
  def self.in_threads(options={:count => 2})
8
8
  count, options = extract_count_from_options(options)
@@ -23,12 +23,7 @@ class Parallel
23
23
  def self.in_processes(options = {}, &block)
24
24
  count, options = extract_count_from_options(options)
25
25
  count ||= processor_count
26
- preserve_results = (options[:preserve_results] != false)
27
-
28
- pipes, pids = fork_and_start_writing(count, :preserve_results => preserve_results, &block)
29
- out = read_from_pipes(pipes)
30
- pids.each { |pid| Process.wait(pid) }
31
- out.map{|x| deserialize(x) } if preserve_results
26
+ map(0...count, options.merge(:in_processes => count), &block)
32
27
  end
33
28
 
34
29
  def self.each(array, options={}, &block)
@@ -40,7 +35,7 @@ class Parallel
40
35
  each(array, options.merge(:with_index => true), &block)
41
36
  end
42
37
 
43
- def self.map(array, options = {})
38
+ def self.map(array, options = {}, &block)
44
39
  array = array.to_a if array.is_a?(Range)
45
40
 
46
41
  if options[:in_threads]
@@ -50,26 +45,26 @@ class Parallel
50
45
  method = :in_processes
51
46
  size = options[method] || processor_count
52
47
  end
53
-
54
- # work in #{size} threads that use threads/processes
55
- results = []
56
- current = -1
57
-
58
- in_threads(size) do
59
- # as long as there are more items, work on one of them
60
- loop do
61
- index = Thread.exclusive{ current+=1 }
62
- break if index >= array.size
63
- results[index] = *send(method, options.merge(:count => 1)) do
64
- args = [array[index]]
65
- args << index if options[:with_index]
66
- yield *args
48
+ size = [array.size, size].min
49
+
50
+ if method == :in_threads
51
+ # work in #{size} threads that use threads/processes
52
+ results = []
53
+ current = -1
54
+
55
+ in_threads(size) do
56
+ # as long as there are more items, work on one of them
57
+ loop do
58
+ index = Thread.exclusive{ current+=1 }
59
+ break if index >= array.size
60
+ results[index] = call_with_index(array, index, options, &block)
67
61
  end
68
62
  end
69
- end
70
63
 
71
- results = results.flatten(1) if SPLAT_BUG
72
- results
64
+ results
65
+ else
66
+ work_in_processes(array, options.merge(:count => size), &block)
67
+ end
73
68
  end
74
69
 
75
70
  def self.map_with_index(array, options={}, &block)
@@ -91,51 +86,117 @@ class Parallel
91
86
 
92
87
  private
93
88
 
94
- # Collect results from pipes simultanously
95
- # otherwise pipes get stuck when to much is written (buffer full)
96
- def self.read_from_pipes(reads)
97
- out = []
98
- in_threads(reads.size) do |i|
99
- out[i] = ''
100
- while text = reads[i].gets
101
- out[i] += text
89
+ def self.work_in_processes(items, options, &blk)
90
+ workers = Array.new(options[:count]).map{ worker(items, options, &blk) }
91
+ Parallel.kill_on_ctrl_c(workers.map{|worker| worker[:pid] })
92
+
93
+ current_index = -1
94
+
95
+ # give every worker something to do
96
+ workers.each do |worker|
97
+ write_to_pipe(worker[:write], current_index += 1)
98
+ end
99
+
100
+ # fetch results and hand out new work
101
+ listener_threads = []
102
+ result = Array.new(items.size)
103
+
104
+ workers.each do |worker|
105
+ listener_threads << Thread.new do
106
+ begin
107
+ while output = worker[:read].gets
108
+ # store output from worker
109
+ result_index, output = decode(output.chomp)
110
+ raise output.exception if ExceptionWrapper === output
111
+ result[result_index] = output
112
+
113
+ # give worker next item
114
+ next_index = Thread.exclusive{ current_index += 1 }
115
+ break if next_index >= items.size
116
+ write_to_pipe(worker[:write], next_index)
117
+ end
118
+ ensure
119
+ worker[:read].close
120
+ worker[:write].close
121
+ end
102
122
  end
103
- reads[i].close
104
123
  end
105
- out
124
+
125
+ wait_for_threads(listener_threads)
126
+
127
+ # if they go zombie, rather wait here to be able to debug
128
+ wait_for_processes(workers.map{|worker| worker[:pid] })
129
+
130
+ result
106
131
  end
107
132
 
108
- # fork and start writing results into n pipes
109
- def self.fork_and_start_writing(count, options, &block)
110
- reads = []
111
- pids = []
112
- count.times do |i|
113
- reads[i], write = IO.pipe
114
- pids << do_in_new_process(i, options.merge(:write_to => (options[:preserve_results] ? write : nil)), &block)
115
- write.close
133
+ def self.worker(items, options, &block)
134
+ # use less memory on REE
135
+ GC.copy_on_write_friendly = true if GC.respond_to?(:copy_on_write_friendly=)
136
+
137
+ child_read, parent_write = IO.pipe
138
+ parent_read, child_write = IO.pipe
139
+
140
+ pid = Process.fork do
141
+ parent_write.close
142
+ parent_read.close
143
+
144
+ begin
145
+ while input = child_read.gets and input != "\n"
146
+ index = decode(input.chomp)
147
+ begin
148
+ result = Parallel.call_with_index(items, index, options, &block)
149
+ result = nil if options[:preserve_results] == false
150
+ rescue Exception => e
151
+ result = ExceptionWrapper.new(e)
152
+ end
153
+ write_to_pipe(child_write, [index, result])
154
+ end
155
+ rescue Interrupt
156
+ child_read.close
157
+ child_write.close
158
+ end
116
159
  end
117
- kill_on_ctrl_c(pids)
118
- [reads, pids]
160
+
161
+ child_read.close
162
+ child_write.close
163
+
164
+ {:read => parent_read, :write => parent_write, :pid => pid}
119
165
  end
120
166
 
121
- def self.do_in_new_process(work_item, options)
122
- # activate copy on write friendly GC of REE
123
- GC.copy_on_write_friendly = true if GC.respond_to?(:copy_on_write_friendly=)
124
- Process.fork do
125
- result = yield(work_item)
126
- serialize(result, options) if options[:write_to]
167
+ def self.write_to_pipe(pipe, item)
168
+ pipe.write(encode(item))
169
+ end
170
+
171
+ def self.wait_for_threads(threads)
172
+ threads.each do |t|
173
+ begin
174
+ t.join
175
+ rescue Interrupt
176
+ # thread died, do not stop other threads
177
+ end
178
+ end
179
+ end
180
+
181
+ def self.wait_for_processes(pids)
182
+ pids.each do |pid|
183
+ begin
184
+ Process.wait(pid)
185
+ rescue Interrupt
186
+ # process died
187
+ end
127
188
  end
128
189
  end
129
190
 
130
- def self.serialize(something, options)
131
- Marshal.dump(something, options[:write_to])
191
+ def self.encode(obj)
192
+ Base64.encode64(Marshal.dump(obj)).split("\n").join + "\n"
132
193
  end
133
194
 
134
- def self.deserialize(something)
135
- Marshal.load(something)
195
+ def self.decode(str)
196
+ Marshal.load(Base64.decode64(str))
136
197
  end
137
198
 
138
- # options is either a Interger or a Hash with :count
199
+ # options is either a Integer or a Hash with :count
139
200
  def self.extract_count_from_options(options)
140
201
  if options.is_a?(Hash)
141
202
  count = options[:count]
@@ -146,21 +207,6 @@ class Parallel
146
207
  [count, options]
147
208
  end
148
209
 
149
- # split an array into groups of size items
150
- # (copied from ActiveSupport, to not require it)
151
- def self.in_groups_of(array, size)
152
- results = []
153
- loop do
154
- slice = array[(results.size * size)...((results.size+1) * size)]
155
- if slice.nil? or slice.empty?
156
- break
157
- else
158
- results << slice
159
- end
160
- end
161
- results
162
- end
163
-
164
210
  # kill all these processes (children) if user presses Ctrl+c
165
211
  def self.kill_on_ctrl_c(pids)
166
212
  Signal.trap :SIGINT do
@@ -169,4 +215,17 @@ class Parallel
169
215
  exit 1 # Quit with 'failed' signal
170
216
  end
171
217
  end
172
- end
218
+
219
+ def self.call_with_index(array, index, options, &block)
220
+ args = [array[index]]
221
+ args << index if options[:with_index]
222
+ block.call(*args)
223
+ end
224
+
225
+ class ExceptionWrapper
226
+ attr_reader :exception
227
+ def initialize(exception)
228
+ @exception = exception
229
+ end
230
+ end
231
+ end
data/parallel.gemspec CHANGED
@@ -5,19 +5,21 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{parallel}
8
- s.version = "0.4.6"
8
+ s.version = "0.5.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Michael Grosser"]
12
- s.date = %q{2010-10-06}
12
+ s.date = %q{2010-10-24}
13
13
  s.email = %q{grosser.michael@gmail.com}
14
14
  s.files = [
15
- "Rakefile",
15
+ "Gemfile",
16
+ "Gemfile.lock",
17
+ "Rakefile",
16
18
  "Readme.md",
17
19
  "VERSION",
18
20
  "lib/parallel.rb",
19
21
  "parallel.gemspec",
20
- "spec/cases/cloeses_processes_at_runtime.rb",
22
+ "spec/cases/closes_processes_at_runtime.rb",
21
23
  "spec/cases/each.rb",
22
24
  "spec/cases/each_with_index.rb",
23
25
  "spec/cases/map_with_index.rb",
@@ -42,7 +44,7 @@ Gem::Specification.new do |s|
42
44
  s.homepage = %q{http://github.com/grosser/parallel}
43
45
  s.rdoc_options = ["--charset=UTF-8"]
44
46
  s.require_paths = ["lib"]
45
- s.rubygems_version = %q{1.3.6}
47
+ s.rubygems_version = %q{1.3.7}
46
48
  s.summary = %q{Run any kind of code in parallel processes}
47
49
  s.test_files = [
48
50
  "spec/spec_helper.rb",
@@ -57,8 +59,8 @@ Gem::Specification.new do |s|
57
59
  "spec/cases/parallel_start_and_kill.rb",
58
60
  "spec/cases/parallel_map_uneven.rb",
59
61
  "spec/cases/parallel_map_sleeping.rb",
60
- "spec/cases/cloeses_processes_at_runtime.rb",
61
62
  "spec/cases/parallel_with_detected_cpus.rb",
63
+ "spec/cases/closes_processes_at_runtime.rb",
62
64
  "spec/cases/each.rb",
63
65
  "spec/cases/map_with_nested_arrays_and_nil.rb",
64
66
  "spec/cases/map_with_index_empty.rb",
@@ -72,7 +74,7 @@ Gem::Specification.new do |s|
72
74
  current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
73
75
  s.specification_version = 3
74
76
 
75
- if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
77
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
76
78
  else
77
79
  end
78
80
  else
@@ -10,7 +10,12 @@ class NotDumpable
10
10
  end
11
11
  end
12
12
 
13
- Parallel.each([NotDumpable.new]) do |x|
14
- print 'not dumpable'
15
- x
13
+ Parallel.each([1]) do
14
+ print 'no dump for result'
15
+ NotDumpable.new
16
+ end
17
+
18
+ Parallel.each([NotDumpable.new]) do
19
+ print 'no dump for each'
20
+ 1
16
21
  end
@@ -30,9 +30,10 @@ describe Parallel do
30
30
  end
31
31
  sleep 1
32
32
  running_processes = `ps -f`.split("\n").map{ |line| line.split(/\s+/) }
33
- uid_index = running_processes.detect{ |line| line.include?("UID") }.index("UID") + 1
34
- parent = running_processes.detect{ |line| line.grep(/(0|)0:00(:|.)00/).any? and line.include?("ruby") }[uid_index]
35
- `kill -2 #{parent}` #simulates Ctrl+c
33
+ pid_index = running_processes.detect{ |line| line.include?("UID") }.index("UID") + 1
34
+ parent_pid = running_processes.detect{ |line| line.grep(/(0|)0:00(:|.)00/).any? and line.include?("ruby") }[pid_index]
35
+ `kill -2 #{parent_pid}` #simulates Ctrl+c
36
+ sleep 1
36
37
  }.should_not change{`ps`.split("\n").size}
37
38
  Time.now.should be_close(t, 3)
38
39
  end
@@ -44,8 +45,7 @@ describe Parallel do
44
45
  end
45
46
 
46
47
  it "raises when one of the processes raises" do
47
- pending 'there is some kind of error, but not the original...'
48
- `ruby spec/cases/parallel_raise.rb`.should == 'TEST'
48
+ `ruby spec/cases/parallel_raise.rb`.strip.should == 'TEST'
49
49
  end
50
50
 
51
51
  it 'can handle to high fork rate' do
@@ -53,7 +53,7 @@ describe Parallel do
53
53
  end
54
54
 
55
55
  it 'it does not leave processes behind while running' do
56
- `ruby spec/cases/cloeses_processes_at_runtime.rb`.should == 'OK'
56
+ `ruby spec/cases/closes_processes_at_runtime.rb`.should == 'OK'
57
57
  end
58
58
  end
59
59
 
@@ -127,7 +127,7 @@ describe Parallel do
127
127
  end
128
128
 
129
129
  it "does not use marshal_dump" do
130
- `ruby spec/cases/no_dump_with_each.rb 2>&1`.should == 'not dumpable'
130
+ `ruby spec/cases/no_dump_with_each.rb 2>&1`.should == 'no dump for resultno dump for each'
131
131
  end
132
132
  end
133
133
 
@@ -136,22 +136,4 @@ describe Parallel do
136
136
  `ruby spec/cases/each_with_index.rb 2>&1`.should == 'a0b1'
137
137
  end
138
138
  end
139
-
140
- describe :in_groups_of do
141
- it "works for empty" do
142
- Parallel.send(:in_groups_of, [], 3).should == []
143
- end
144
-
145
- it "works for smaller then count" do
146
- Parallel.send(:in_groups_of, [1,2], 3).should == [[1,2]]
147
- end
148
-
149
- it "works for count" do
150
- Parallel.send(:in_groups_of, [1,2,3], 3).should == [[1,2,3]]
151
- end
152
-
153
- it "works for larger than count" do
154
- Parallel.send(:in_groups_of, [1,2,3,4], 3).should == [[1,2,3],[4]]
155
- end
156
- end
157
139
  end
metadata CHANGED
@@ -1,12 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: parallel
3
3
  version: !ruby/object:Gem::Version
4
+ hash: 11
4
5
  prerelease: false
5
6
  segments:
6
7
  - 0
7
- - 4
8
- - 6
9
- version: 0.4.6
8
+ - 5
9
+ - 0
10
+ version: 0.5.0
10
11
  platform: ruby
11
12
  authors:
12
13
  - Michael Grosser
@@ -14,7 +15,7 @@ autorequire:
14
15
  bindir: bin
15
16
  cert_chain: []
16
17
 
17
- date: 2010-10-06 00:00:00 +02:00
18
+ date: 2010-10-24 00:00:00 +02:00
18
19
  default_executable:
19
20
  dependencies: []
20
21
 
@@ -27,12 +28,14 @@ extensions: []
27
28
  extra_rdoc_files: []
28
29
 
29
30
  files:
31
+ - Gemfile
32
+ - Gemfile.lock
30
33
  - Rakefile
31
34
  - Readme.md
32
35
  - VERSION
33
36
  - lib/parallel.rb
34
37
  - parallel.gemspec
35
- - spec/cases/cloeses_processes_at_runtime.rb
38
+ - spec/cases/closes_processes_at_runtime.rb
36
39
  - spec/cases/each.rb
37
40
  - spec/cases/each_with_index.rb
38
41
  - spec/cases/map_with_index.rb
@@ -63,23 +66,27 @@ rdoc_options:
63
66
  require_paths:
64
67
  - lib
65
68
  required_ruby_version: !ruby/object:Gem::Requirement
69
+ none: false
66
70
  requirements:
67
71
  - - ">="
68
72
  - !ruby/object:Gem::Version
73
+ hash: 3
69
74
  segments:
70
75
  - 0
71
76
  version: "0"
72
77
  required_rubygems_version: !ruby/object:Gem::Requirement
78
+ none: false
73
79
  requirements:
74
80
  - - ">="
75
81
  - !ruby/object:Gem::Version
82
+ hash: 3
76
83
  segments:
77
84
  - 0
78
85
  version: "0"
79
86
  requirements: []
80
87
 
81
88
  rubyforge_project:
82
- rubygems_version: 1.3.6
89
+ rubygems_version: 1.3.7
83
90
  signing_key:
84
91
  specification_version: 3
85
92
  summary: Run any kind of code in parallel processes
@@ -96,8 +103,8 @@ test_files:
96
103
  - spec/cases/parallel_start_and_kill.rb
97
104
  - spec/cases/parallel_map_uneven.rb
98
105
  - spec/cases/parallel_map_sleeping.rb
99
- - spec/cases/cloeses_processes_at_runtime.rb
100
106
  - spec/cases/parallel_with_detected_cpus.rb
107
+ - spec/cases/closes_processes_at_runtime.rb
101
108
  - spec/cases/each.rb
102
109
  - spec/cases/map_with_nested_arrays_and_nil.rb
103
110
  - spec/cases/map_with_index_empty.rb