parallel 0.4.6 → 0.5.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/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