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 +5 -0
- data/Gemfile.lock +22 -0
- data/Readme.md +1 -0
- data/VERSION +1 -1
- data/lib/parallel.rb +131 -72
- data/parallel.gemspec +9 -7
- data/spec/cases/{cloeses_processes_at_runtime.rb → closes_processes_at_runtime.rb} +0 -0
- data/spec/cases/no_dump_with_each.rb +8 -3
- data/spec/parallel_spec.rb +7 -25
- metadata +14 -7
data/Gemfile
ADDED
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.
|
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
|
-
|
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
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
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
|
-
|
72
|
-
|
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
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
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
|
-
|
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
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
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
|
-
|
118
|
-
|
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.
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
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.
|
131
|
-
Marshal.dump(
|
191
|
+
def self.encode(obj)
|
192
|
+
Base64.encode64(Marshal.dump(obj)).split("\n").join + "\n"
|
132
193
|
end
|
133
194
|
|
134
|
-
def self.
|
135
|
-
Marshal.load(
|
195
|
+
def self.decode(str)
|
196
|
+
Marshal.load(Base64.decode64(str))
|
136
197
|
end
|
137
198
|
|
138
|
-
# options is either a
|
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
|
-
|
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.
|
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-
|
12
|
+
s.date = %q{2010-10-24}
|
13
13
|
s.email = %q{grosser.michael@gmail.com}
|
14
14
|
s.files = [
|
15
|
-
"
|
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/
|
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.
|
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::
|
77
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
76
78
|
else
|
77
79
|
end
|
78
80
|
else
|
File without changes
|
@@ -10,7 +10,12 @@ class NotDumpable
|
|
10
10
|
end
|
11
11
|
end
|
12
12
|
|
13
|
-
Parallel.each([
|
14
|
-
print '
|
15
|
-
|
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
|
data/spec/parallel_spec.rb
CHANGED
@@ -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
|
-
|
34
|
-
|
35
|
-
`kill -2 #{
|
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
|
-
|
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/
|
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 == '
|
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
|
-
-
|
8
|
-
-
|
9
|
-
version: 0.
|
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-
|
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/
|
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.
|
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
|