parallel 0.3.1
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/README.markdown +63 -0
- data/Rakefile +26 -0
- data/VERSION +1 -0
- data/lib/parallel.rb +113 -0
- data/parallel.gemspec +64 -0
- data/spec/cases/parallel_influence_outside_data.rb +8 -0
- data/spec/cases/parallel_map.rb +6 -0
- data/spec/cases/parallel_map_sleeping.rb +5 -0
- data/spec/cases/parallel_map_uneven.rb +5 -0
- data/spec/cases/parallel_raise.rb +10 -0
- data/spec/cases/parallel_sleeping_2.rb +5 -0
- data/spec/cases/parallel_start_and_kill.rb +6 -0
- data/spec/cases/parallel_with_detected_cpus.rb +6 -0
- data/spec/cases/parallel_with_set_processes.rb +6 -0
- data/spec/parallel_spec.rb +110 -0
- data/spec/spec_helper.rb +4 -0
- metadata +80 -0
data/README.markdown
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
Run any kind of code in parallel Processes or Threads, to speedup computation by factor #{your_cpus} X.
|
2
|
+
|
3
|
+
- Child processes are killed when your main process is killed through Ctrl+c or kill -2
|
4
|
+
|
5
|
+
Install
|
6
|
+
=======
|
7
|
+
sudo gem install grosser-parallel -s http://gems.github.com/
|
8
|
+
|
9
|
+
Usage
|
10
|
+
=====
|
11
|
+
### Processes
|
12
|
+
- Speedup through multiple CPUs
|
13
|
+
- Speedup for blocking operations
|
14
|
+
- Protects global data
|
15
|
+
- Extra memory used
|
16
|
+
|
17
|
+
### Threads
|
18
|
+
- Speedup for blocking operations
|
19
|
+
- Global data can be modified
|
20
|
+
- No extra memory used
|
21
|
+
|
22
|
+
Map-Reduce-Style
|
23
|
+
# 2 CPUs -> finished after 2 runs (a,b + c)
|
24
|
+
results = Parallel.map(['a','b','c']) do |one_letter|
|
25
|
+
expensive_calculation(letter)
|
26
|
+
end
|
27
|
+
|
28
|
+
# 3 Processes -> finished after 1 run
|
29
|
+
results = Parallel.map(['a','b','c'], :in_processes=>3){|one_letter| ... }
|
30
|
+
|
31
|
+
# 3 Threads -> finished after 1 run
|
32
|
+
results = Parallel.map(['a','b','c'], :in_threads=>3){|one_letter| ... }
|
33
|
+
|
34
|
+
|
35
|
+
Normal
|
36
|
+
#i -> 0...number_of_your_cpus
|
37
|
+
results = Parallel.in_processes do |i|
|
38
|
+
expensive_computation(data[i])
|
39
|
+
end
|
40
|
+
|
41
|
+
#i -> 0...4
|
42
|
+
results = Parallel.in_processes(4) do |i|
|
43
|
+
expensive_computation(data[i])
|
44
|
+
end
|
45
|
+
|
46
|
+
# Threads
|
47
|
+
results = Parallel.in_threads(4) do |i|
|
48
|
+
blocking_computation(data[i])
|
49
|
+
end
|
50
|
+
|
51
|
+
TODO
|
52
|
+
====
|
53
|
+
- JRuby / Windows support <-> possible ?
|
54
|
+
|
55
|
+
Authors
|
56
|
+
=======
|
57
|
+
|
58
|
+
###Contributors (alphabetical)
|
59
|
+
- [TJ Holowaychuk](http://vision-media.ca/) -- tj<$at$>vision-media.ca
|
60
|
+
|
61
|
+
[Michael Grosser](http://pragmatig.wordpress.com)
|
62
|
+
grosser.michael@gmail.com
|
63
|
+
Hereby placed under public domain, do what you want, just do not hold me accountable...
|
data/Rakefile
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
desc "Run all specs in spec directory"
|
2
|
+
task :default do
|
3
|
+
options = "--colour --format progress --loadby --reverse"
|
4
|
+
files = FileList['spec/**/*_spec.rb']
|
5
|
+
system("spec #{options} #{files}")
|
6
|
+
end
|
7
|
+
|
8
|
+
# fake task so that rubyforge:release works
|
9
|
+
task :rdoc do
|
10
|
+
end
|
11
|
+
|
12
|
+
begin
|
13
|
+
require 'jeweler'
|
14
|
+
project_name = 'parallel'
|
15
|
+
Jeweler::Tasks.new do |gem|
|
16
|
+
gem.name = project_name
|
17
|
+
gem.summary = "Run any kind of code in parallel processes"
|
18
|
+
gem.email = "grosser.michael@gmail.com"
|
19
|
+
gem.homepage = "http://github.com/grosser/#{project_name}"
|
20
|
+
gem.authors = ["Michael Grosser"]
|
21
|
+
gem.rubyforge_project = 'parallel'
|
22
|
+
end
|
23
|
+
Jeweler::RubyforgeTasks.new
|
24
|
+
rescue LoadError
|
25
|
+
puts "Jeweler, or one of its dependencies, is not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
|
26
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.3.1
|
data/lib/parallel.rb
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
class Parallel
|
2
|
+
VERSION = File.read( File.join(File.dirname(__FILE__),'..','VERSION') ).strip
|
3
|
+
|
4
|
+
def self.in_threads(count = 2)
|
5
|
+
out = []
|
6
|
+
threads = []
|
7
|
+
|
8
|
+
count.times do |i|
|
9
|
+
threads[i] = Thread.new do
|
10
|
+
out[i] = yield(i)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
threads.each{|t| t.join }
|
15
|
+
out
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.in_processes(count = processor_count)
|
19
|
+
# Start writing results into n pipes
|
20
|
+
reads = []
|
21
|
+
writes = []
|
22
|
+
pids = []
|
23
|
+
count.times do |i|
|
24
|
+
reads[i], writes[i] = IO.pipe
|
25
|
+
pids << Process.fork do
|
26
|
+
Marshal.dump(yield(i), writes[i]) # Serialize result
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
kill_on_ctrl_c(pids)
|
31
|
+
|
32
|
+
# Collect results from pipes simultanously
|
33
|
+
# otherwise pipes get stuck when to much is written (buffer full)
|
34
|
+
out = []
|
35
|
+
collectors = []
|
36
|
+
count.times do |i|
|
37
|
+
collectors << Thread.new do
|
38
|
+
writes[i].close
|
39
|
+
|
40
|
+
out[i] = ''
|
41
|
+
while text = reads[i].gets
|
42
|
+
out[i] += text
|
43
|
+
end
|
44
|
+
|
45
|
+
reads[i].close
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
collectors.each{|c| c.join }
|
50
|
+
|
51
|
+
out.map{|x| Marshal.load(x) } # Deserialize results
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.map(array, options = {})
|
55
|
+
require 'thread' # to get Thread.exclusive
|
56
|
+
|
57
|
+
if options[:in_threads]
|
58
|
+
method = :in_threads
|
59
|
+
size = options[method]
|
60
|
+
else
|
61
|
+
method = :in_processes
|
62
|
+
size = options[method] || processor_count
|
63
|
+
end
|
64
|
+
|
65
|
+
# work in #{size} threads that use threads/processes
|
66
|
+
results = []
|
67
|
+
current = -1
|
68
|
+
|
69
|
+
in_threads(size) do
|
70
|
+
# as long as there are more items, work on one of them
|
71
|
+
loop do
|
72
|
+
index = Thread.exclusive{ current+=1 }
|
73
|
+
break if index >= array.size
|
74
|
+
results[index] = *send(method, 1){ yield array[index] }
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
results
|
79
|
+
end
|
80
|
+
|
81
|
+
def self.processor_count
|
82
|
+
case RUBY_PLATFORM
|
83
|
+
when /darwin/
|
84
|
+
`hwprefs cpu_count`.to_i
|
85
|
+
when /linux/
|
86
|
+
`cat /proc/cpuinfo | grep processor | wc -l`.to_i
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
def self.in_groups_of(array, size)
|
93
|
+
results = []
|
94
|
+
loop do
|
95
|
+
slice = array[(results.size * size)...((results.size+1) * size)]
|
96
|
+
if slice.nil? or slice.empty?
|
97
|
+
break
|
98
|
+
else
|
99
|
+
results << slice
|
100
|
+
end
|
101
|
+
end
|
102
|
+
results
|
103
|
+
end
|
104
|
+
|
105
|
+
#handle user interrup (Ctrl+c)
|
106
|
+
def self.kill_on_ctrl_c(pids)
|
107
|
+
Signal.trap :SIGINT do
|
108
|
+
$stderr.puts 'Parallel execution interrupted, exiting ...'
|
109
|
+
pids.each { |pid| Process.kill(:KILL, pid) }
|
110
|
+
exit 1 # Quit with 'failed' signal
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
data/parallel.gemspec
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run `rake gemspec`
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{parallel}
|
8
|
+
s.version = "0.3.1"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Michael Grosser"]
|
12
|
+
s.date = %q{2009-10-04}
|
13
|
+
s.email = %q{grosser.michael@gmail.com}
|
14
|
+
s.extra_rdoc_files = [
|
15
|
+
"README.markdown"
|
16
|
+
]
|
17
|
+
s.files = [
|
18
|
+
"README.markdown",
|
19
|
+
"Rakefile",
|
20
|
+
"VERSION",
|
21
|
+
"lib/parallel.rb",
|
22
|
+
"parallel.gemspec",
|
23
|
+
"spec/cases/parallel_influence_outside_data.rb",
|
24
|
+
"spec/cases/parallel_map.rb",
|
25
|
+
"spec/cases/parallel_map_sleeping.rb",
|
26
|
+
"spec/cases/parallel_map_uneven.rb",
|
27
|
+
"spec/cases/parallel_raise.rb",
|
28
|
+
"spec/cases/parallel_sleeping_2.rb",
|
29
|
+
"spec/cases/parallel_start_and_kill.rb",
|
30
|
+
"spec/cases/parallel_with_detected_cpus.rb",
|
31
|
+
"spec/cases/parallel_with_set_processes.rb",
|
32
|
+
"spec/parallel_spec.rb",
|
33
|
+
"spec/spec_helper.rb"
|
34
|
+
]
|
35
|
+
s.homepage = %q{http://github.com/grosser/parallel}
|
36
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
37
|
+
s.require_paths = ["lib"]
|
38
|
+
s.rubyforge_project = %q{parallel}
|
39
|
+
s.rubygems_version = %q{1.3.5}
|
40
|
+
s.summary = %q{Run any kind of code in parallel processes}
|
41
|
+
s.test_files = [
|
42
|
+
"spec/parallel_spec.rb",
|
43
|
+
"spec/spec_helper.rb",
|
44
|
+
"spec/cases/parallel_raise.rb",
|
45
|
+
"spec/cases/parallel_sleeping_2.rb",
|
46
|
+
"spec/cases/parallel_start_and_kill.rb",
|
47
|
+
"spec/cases/parallel_with_set_processes.rb",
|
48
|
+
"spec/cases/parallel_influence_outside_data.rb",
|
49
|
+
"spec/cases/parallel_map_sleeping.rb",
|
50
|
+
"spec/cases/parallel_map_uneven.rb",
|
51
|
+
"spec/cases/parallel_with_detected_cpus.rb",
|
52
|
+
"spec/cases/parallel_map.rb"
|
53
|
+
]
|
54
|
+
|
55
|
+
if s.respond_to? :specification_version then
|
56
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
57
|
+
s.specification_version = 3
|
58
|
+
|
59
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
60
|
+
else
|
61
|
+
end
|
62
|
+
else
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
|
3
|
+
describe Parallel do
|
4
|
+
describe :in_processes do
|
5
|
+
before do
|
6
|
+
@cpus = Parallel.processor_count
|
7
|
+
end
|
8
|
+
|
9
|
+
it "executes with detected cpus" do
|
10
|
+
`ruby spec/cases/parallel_with_detected_cpus.rb`.should == "HELLO\n" * @cpus
|
11
|
+
end
|
12
|
+
|
13
|
+
it "set ammount of parallel processes" do
|
14
|
+
`ruby spec/cases/parallel_with_set_processes.rb`.should == "HELLO\n" * 5
|
15
|
+
end
|
16
|
+
|
17
|
+
it "does not influence outside data" do
|
18
|
+
`ruby spec/cases/parallel_influence_outside_data.rb`.should == "yes"
|
19
|
+
end
|
20
|
+
|
21
|
+
it "kills the processes when the main process gets killed through ctrl+c" do
|
22
|
+
t = Time.now
|
23
|
+
lambda{
|
24
|
+
Thread.new do
|
25
|
+
`ruby spec/cases/parallel_start_and_kill.rb`
|
26
|
+
end
|
27
|
+
sleep 1
|
28
|
+
running_processes = `ps -f`.split("\n").map{|line| line.split(/\s+/)}
|
29
|
+
parent = running_processes.detect{|line| line.include?("00:00:00") and line.include?("ruby") }[1]
|
30
|
+
`kill -2 #{parent}` #simulates Ctrl+c
|
31
|
+
}.should_not change{`ps`.split("\n").size}
|
32
|
+
Time.now.should be_close(t, 3)
|
33
|
+
end
|
34
|
+
|
35
|
+
it "saves time" do
|
36
|
+
t = Time.now
|
37
|
+
`ruby spec/cases/parallel_sleeping_2.rb`
|
38
|
+
Time.now.should be_close(t, 3)
|
39
|
+
end
|
40
|
+
|
41
|
+
it "raises when one of the processes raises" do
|
42
|
+
pending 'there is some kind of error, but not the original...'
|
43
|
+
`ruby spec/cases/parallel_raise.rb`.should == 'TEST'
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe :in_threads do
|
48
|
+
it "saves time" do
|
49
|
+
t = Time.now
|
50
|
+
Parallel.in_threads(3){ sleep 2 }
|
51
|
+
Time.now.should be_close(t, 3)
|
52
|
+
end
|
53
|
+
|
54
|
+
it "does not create new processes" do
|
55
|
+
lambda{ Thread.new{ Parallel.in_threads(2){sleep 1} } }.should_not change{`ps`.split("\n").size}
|
56
|
+
end
|
57
|
+
|
58
|
+
it "returns results as array" do
|
59
|
+
Parallel.in_threads(4){|i| "XXX#{i}"}.should == ["XXX0",'XXX1','XXX2','XXX3']
|
60
|
+
end
|
61
|
+
|
62
|
+
it "raises when a thread raises" do
|
63
|
+
lambda{ Parallel.in_threads(2){|i| raise "TEST"} }.should raise_error("TEST")
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
describe :map do
|
68
|
+
it "saves time" do
|
69
|
+
t = Time.now
|
70
|
+
`ruby spec/cases/parallel_map_sleeping.rb`
|
71
|
+
Time.now.should be_close(t, 3)
|
72
|
+
end
|
73
|
+
|
74
|
+
it "executes with given parameters" do
|
75
|
+
`ruby spec/cases/parallel_map.rb`.should == "-a- -b- -c- -d-"
|
76
|
+
end
|
77
|
+
|
78
|
+
it "starts new process imediatly when old exists" do
|
79
|
+
t = Time.now
|
80
|
+
`ruby spec/cases/parallel_map_uneven.rb`
|
81
|
+
Time.now.should be_close(t, 3)
|
82
|
+
end
|
83
|
+
|
84
|
+
it "does not flatten results" do
|
85
|
+
Parallel.map([1,2,3], :in_threads=>2){|x| [x,x]}.should == [[1,1],[2,2],[3,3]]
|
86
|
+
end
|
87
|
+
|
88
|
+
it "can run in threads" do
|
89
|
+
Parallel.map([1,2,3,4,5,6,7,8,9], :in_threads=>4){|x| x+2 }.should == [3,4,5,6,7,8,9,10,11]
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
describe :in_groups_of do
|
94
|
+
it "works for empty" do
|
95
|
+
Parallel.send(:in_groups_of, [], 3).should == []
|
96
|
+
end
|
97
|
+
|
98
|
+
it "works for smaller then count" do
|
99
|
+
Parallel.send(:in_groups_of, [1,2], 3).should == [[1,2]]
|
100
|
+
end
|
101
|
+
|
102
|
+
it "works for count" do
|
103
|
+
Parallel.send(:in_groups_of, [1,2,3], 3).should == [[1,2,3]]
|
104
|
+
end
|
105
|
+
|
106
|
+
it "works for larger than count" do
|
107
|
+
Parallel.send(:in_groups_of, [1,2,3,4], 3).should == [[1,2,3],[4]]
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: parallel
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.3.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Michael Grosser
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-10-04 00:00:00 +02:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description:
|
17
|
+
email: grosser.michael@gmail.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files:
|
23
|
+
- README.markdown
|
24
|
+
files:
|
25
|
+
- README.markdown
|
26
|
+
- Rakefile
|
27
|
+
- VERSION
|
28
|
+
- lib/parallel.rb
|
29
|
+
- parallel.gemspec
|
30
|
+
- spec/cases/parallel_influence_outside_data.rb
|
31
|
+
- spec/cases/parallel_map.rb
|
32
|
+
- spec/cases/parallel_map_sleeping.rb
|
33
|
+
- spec/cases/parallel_map_uneven.rb
|
34
|
+
- spec/cases/parallel_raise.rb
|
35
|
+
- spec/cases/parallel_sleeping_2.rb
|
36
|
+
- spec/cases/parallel_start_and_kill.rb
|
37
|
+
- spec/cases/parallel_with_detected_cpus.rb
|
38
|
+
- spec/cases/parallel_with_set_processes.rb
|
39
|
+
- spec/parallel_spec.rb
|
40
|
+
- spec/spec_helper.rb
|
41
|
+
has_rdoc: true
|
42
|
+
homepage: http://github.com/grosser/parallel
|
43
|
+
licenses: []
|
44
|
+
|
45
|
+
post_install_message:
|
46
|
+
rdoc_options:
|
47
|
+
- --charset=UTF-8
|
48
|
+
require_paths:
|
49
|
+
- lib
|
50
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: "0"
|
55
|
+
version:
|
56
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: "0"
|
61
|
+
version:
|
62
|
+
requirements: []
|
63
|
+
|
64
|
+
rubyforge_project: parallel
|
65
|
+
rubygems_version: 1.3.5
|
66
|
+
signing_key:
|
67
|
+
specification_version: 3
|
68
|
+
summary: Run any kind of code in parallel processes
|
69
|
+
test_files:
|
70
|
+
- spec/parallel_spec.rb
|
71
|
+
- spec/spec_helper.rb
|
72
|
+
- spec/cases/parallel_raise.rb
|
73
|
+
- spec/cases/parallel_sleeping_2.rb
|
74
|
+
- spec/cases/parallel_start_and_kill.rb
|
75
|
+
- spec/cases/parallel_with_set_processes.rb
|
76
|
+
- spec/cases/parallel_influence_outside_data.rb
|
77
|
+
- spec/cases/parallel_map_sleeping.rb
|
78
|
+
- spec/cases/parallel_map_uneven.rb
|
79
|
+
- spec/cases/parallel_with_detected_cpus.rb
|
80
|
+
- spec/cases/parallel_map.rb
|