agent 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +3 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +27 -0
- data/README.md +73 -0
- data/Rakefile +2 -0
- data/agent.gemspec +22 -0
- data/autotest/discover.rb +1 -0
- data/lib/agent.rb +15 -0
- data/lib/agent/channel.rb +74 -0
- data/lib/agent/transport/queue.rb +82 -0
- data/lib/agent/version.rb +3 -0
- data/spec/channel_spec.rb +145 -0
- data/spec/examples/channel_of_channels_spec.rb +59 -0
- data/spec/examples/go/producer_consumer.go +34 -0
- data/spec/examples/go/sieve.go +76 -0
- data/spec/examples/producer_consumer_spec.rb +83 -0
- data/spec/examples/sieve_spec.rb +143 -0
- data/spec/helper.rb +5 -0
- data/spec/queue_spec.rb +53 -0
- metadata +102 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
agent (0.0.1)
|
5
|
+
|
6
|
+
GEM
|
7
|
+
remote: http://rubygems.org/
|
8
|
+
specs:
|
9
|
+
diff-lcs (1.1.2)
|
10
|
+
rspec (2.0.1)
|
11
|
+
rspec-core (~> 2.0.1)
|
12
|
+
rspec-expectations (~> 2.0.1)
|
13
|
+
rspec-mocks (~> 2.0.1)
|
14
|
+
rspec-core (2.0.1)
|
15
|
+
rspec-expectations (2.0.1)
|
16
|
+
diff-lcs (>= 1.1.2)
|
17
|
+
rspec-mocks (2.0.1)
|
18
|
+
rspec-core (~> 2.0.1)
|
19
|
+
rspec-expectations (~> 2.0.1)
|
20
|
+
|
21
|
+
PLATFORMS
|
22
|
+
java
|
23
|
+
ruby
|
24
|
+
|
25
|
+
DEPENDENCIES
|
26
|
+
agent!
|
27
|
+
rspec
|
data/README.md
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
# Agent
|
2
|
+
|
3
|
+
Agent is a diverse family of related approaches for modelling concurrent systems, in Ruby. In other words, it is a collection of different [process calculi](http://en.wikipedia.org/wiki/Process_calculus) primitives and patterns, with no specific, idiomatic affiliation to any specific implementation. A few available patterns so far:
|
4
|
+
|
5
|
+
- Goroutines on top of green Ruby threads
|
6
|
+
- Named, in-memory channels
|
7
|
+
|
8
|
+
This gem is a work in progress & an experiment, so treat it as such. At the moment, it is heavily influenced by Google's Go and π-calculus primitives.
|
9
|
+
|
10
|
+
# Working Code Examples
|
11
|
+
|
12
|
+
* [Producer, Consumer with Goroutines](https://github.com/igrigorik/agent/blob/master/spec/examples/producer_consumer_spec.rb)
|
13
|
+
* [Serializable Channels / Event-driven server](https://github.com/igrigorik/agent/blob/master/spec/examples/channel_of_channels_spec.rb)
|
14
|
+
* [Sieve of Eratosthenes](https://github.com/igrigorik/agent/blob/master/spec/examples/sieve_spec.rb)
|
15
|
+
|
16
|
+
# Example: Goroutine Generator
|
17
|
+
|
18
|
+
producer = Proc.new do |c|
|
19
|
+
puts "Starting generator: #{c.name}"
|
20
|
+
|
21
|
+
i = 0
|
22
|
+
loop { c.pipe << i+= 1 }
|
23
|
+
end
|
24
|
+
|
25
|
+
c = Agent::Channel.new(name: :incr, type: Integer)
|
26
|
+
|
27
|
+
Generator = Struct.new(:name, :pipe)
|
28
|
+
g = Generator.new(:incr, c)
|
29
|
+
|
30
|
+
go(g, &producer)
|
31
|
+
|
32
|
+
c.receive
|
33
|
+
c.receive
|
34
|
+
|
35
|
+
|
36
|
+
# Go & π-calculus: Background & Motivation
|
37
|
+
|
38
|
+
*Do not communicate by sharing memory; instead, share memory by communicating.*
|
39
|
+
|
40
|
+
Concurrent programming in many environments is made difficult by the subtleties required to implement correct access to shared variables. Google's Go encourages a different approach in which shared values are passed around on channels and, in fact, never actively shared by separate threads of execution. Only one goroutine has access to the value at any given time. Data races cannot occur, by design.
|
41
|
+
|
42
|
+
One way to think about this model is to consider a typical single-threaded program running on one CPU. It has no need for synchronization primitives. Now run another such instance; it too needs no synchronization. Now let those two communicate; if the communication is the synchronizer, there's still no need for other synchronization. Unix pipelines, for example, fit this model perfectly. Although Go's approach to concurrency originates in Hoare's Communicating Sequential Processes (CSP), it can also be seen as a type-safe generalization of Unix pipes.
|
43
|
+
|
44
|
+
To learn more about Go see following resources:
|
45
|
+
|
46
|
+
* [golang.org](http://golang.org/)
|
47
|
+
* [Go's concurrency](http://golang.org/doc/effective_go.html#concurrency)
|
48
|
+
* [Go's channels](http://golang.org/doc/effective_go.html#channels)
|
49
|
+
|
50
|
+
# License
|
51
|
+
|
52
|
+
(The MIT License)
|
53
|
+
|
54
|
+
Copyright (c) 2010 Ilya Grigorik
|
55
|
+
|
56
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
57
|
+
a copy of this software and associated documentation files (the
|
58
|
+
'Software'), to deal in the Software without restriction, including
|
59
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
60
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
61
|
+
permit persons to whom the Software is furnished to do so, subject to
|
62
|
+
the following conditions:
|
63
|
+
|
64
|
+
The above copyright notice and this permission notice shall be
|
65
|
+
included in all copies or substantial portions of the Software.
|
66
|
+
|
67
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
68
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
69
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
70
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
71
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
72
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
73
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
data/agent.gemspec
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "agent/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "agent"
|
7
|
+
s.version = Agent::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["Ilya Grigorik"]
|
10
|
+
s.email = ["ilya@igvita.com"]
|
11
|
+
s.homepage = "https://github.com/igrigorik/agent"
|
12
|
+
s.summary = %q{Agent is a diverse family of related approaches for modelling concurrent systems, in Ruby}
|
13
|
+
s.description = s.summary
|
14
|
+
|
15
|
+
s.rubyforge_project = "agent"
|
16
|
+
s.add_development_dependency "rspec"
|
17
|
+
|
18
|
+
s.files = `git ls-files`.split("\n")
|
19
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
20
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
21
|
+
s.require_paths = ["lib"]
|
22
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
Autotest.add_discovery { 'rspec2' }
|
data/lib/agent.rb
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
# Channels combine communication—the exchange of a value—with synchronization—guaranteeing
|
2
|
+
# that two calculations (goroutines) are in a known state.
|
3
|
+
# - http://golang.org/doc/effective_go.html#channels
|
4
|
+
|
5
|
+
module Agent
|
6
|
+
class Channel
|
7
|
+
attr_reader :name, :transport, :chan
|
8
|
+
|
9
|
+
def initialize(opts = {})
|
10
|
+
@state = :active
|
11
|
+
@name = opts[:name]
|
12
|
+
@max = opts[:size] || 1
|
13
|
+
@type = opts[:type]
|
14
|
+
@direction = opts[:direction] || :bidirectional
|
15
|
+
@transport = opts[:transport] || Agent::Transport::Queue
|
16
|
+
|
17
|
+
raise NoName if @name.nil?
|
18
|
+
raise Untyped if @type.nil?
|
19
|
+
|
20
|
+
@chan = @transport.new(@name, @max)
|
21
|
+
end
|
22
|
+
|
23
|
+
def marshal_load(ary)
|
24
|
+
@state, @name, @type, @direction, @transport = *ary
|
25
|
+
@chan = @transport.new(@name)
|
26
|
+
self
|
27
|
+
end
|
28
|
+
|
29
|
+
def marshal_dump
|
30
|
+
[@state, @name, @type, @direction, @transport]
|
31
|
+
end
|
32
|
+
|
33
|
+
def send(msg)
|
34
|
+
check_direction(:send)
|
35
|
+
check_type(msg)
|
36
|
+
|
37
|
+
@chan.send(Marshal.dump(msg))
|
38
|
+
end
|
39
|
+
alias :push :send
|
40
|
+
alias :<< :send
|
41
|
+
|
42
|
+
def receive
|
43
|
+
check_direction(:receive)
|
44
|
+
|
45
|
+
msg = Marshal.load(@chan.receive)
|
46
|
+
check_type(msg)
|
47
|
+
|
48
|
+
msg
|
49
|
+
end
|
50
|
+
alias :pop :receive
|
51
|
+
|
52
|
+
def closed?; @state == :closed; end
|
53
|
+
def close
|
54
|
+
@chan.close
|
55
|
+
@state = :closed
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def check_type(msg)
|
61
|
+
raise InvalidType if !msg.is_a? @type
|
62
|
+
end
|
63
|
+
|
64
|
+
def check_direction(direction)
|
65
|
+
return if @direction == :bidirectional
|
66
|
+
raise InvalidDirection if @direction != direction
|
67
|
+
end
|
68
|
+
|
69
|
+
class InvalidDirection < Exception; end
|
70
|
+
class NoName < Exception; end
|
71
|
+
class Untyped < Exception; end
|
72
|
+
class InvalidType < Exception; end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
module Agent
|
2
|
+
module Transport
|
3
|
+
|
4
|
+
class Queue
|
5
|
+
|
6
|
+
@@registry = {}
|
7
|
+
|
8
|
+
def initialize(name, max = 1)
|
9
|
+
raise ArgumentError, "queue size must be positive" unless max > 0
|
10
|
+
|
11
|
+
@name = name
|
12
|
+
@max = max
|
13
|
+
|
14
|
+
if !@@registry[@name]
|
15
|
+
@@registry[@name] = {
|
16
|
+
:que => [],
|
17
|
+
:wait => [],
|
18
|
+
:mutex => Mutex.new,
|
19
|
+
:cvar => ConditionVariable.new
|
20
|
+
}
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def data; @@registry[@name]; end
|
25
|
+
def que; data[:que]; end
|
26
|
+
def wait; data[:wait]; end
|
27
|
+
def mutex; data[:mutex]; end
|
28
|
+
def cvar; data[:cvar]; end
|
29
|
+
|
30
|
+
def max; @max; end
|
31
|
+
def size; que.size; end
|
32
|
+
def length; que.size; end
|
33
|
+
|
34
|
+
def push(obj)
|
35
|
+
mutex.synchronize {
|
36
|
+
while true
|
37
|
+
break if que.length < @max
|
38
|
+
cvar.wait(mutex)
|
39
|
+
end
|
40
|
+
|
41
|
+
que.push obj
|
42
|
+
cvar.signal
|
43
|
+
}
|
44
|
+
end
|
45
|
+
alias << push
|
46
|
+
alias enq push
|
47
|
+
|
48
|
+
def pop(*args)
|
49
|
+
mutex.synchronize {
|
50
|
+
while true
|
51
|
+
break if !que.empty?
|
52
|
+
cvar.wait(mutex)
|
53
|
+
end
|
54
|
+
|
55
|
+
retval = que.shift
|
56
|
+
cvar.signal
|
57
|
+
|
58
|
+
retval
|
59
|
+
}
|
60
|
+
end
|
61
|
+
alias shift pop
|
62
|
+
alias deq pop
|
63
|
+
|
64
|
+
def async?; @max > 1; end
|
65
|
+
|
66
|
+
def send(msg, nonblock = false)
|
67
|
+
raise ThreadError, "buffer full" if nonblock && que.length >= @max
|
68
|
+
push(msg)
|
69
|
+
end
|
70
|
+
|
71
|
+
def receive(nonblock = false)
|
72
|
+
raise ThreadError, "buffer empty" if nonblock && que.empty?
|
73
|
+
pop
|
74
|
+
end
|
75
|
+
|
76
|
+
def close
|
77
|
+
@@registry.delete @name
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
require "helper"
|
2
|
+
|
3
|
+
describe Agent::Channel do
|
4
|
+
# http://golang.org/doc/go_spec.html#Channel_types
|
5
|
+
|
6
|
+
include Agent
|
7
|
+
let(:c) { Channel.new(:name => "spec", :type => String) }
|
8
|
+
|
9
|
+
it "should have a name" do
|
10
|
+
lambda { Channel.new(:type => String) }.should raise_error(Channel::NoName)
|
11
|
+
c.name.should == "spec"
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should respond to close" do
|
15
|
+
lambda { c.close }.should_not raise_error
|
16
|
+
c.closed?.should be_true
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should respond to closed?" do
|
20
|
+
c.closed?.should be_false
|
21
|
+
c.close
|
22
|
+
c.closed?.should be_true
|
23
|
+
end
|
24
|
+
|
25
|
+
context "deadlock" do
|
26
|
+
it "should deadlock on single thread" do
|
27
|
+
c = Channel.new(:name => "deadlock", :type => String)
|
28
|
+
lambda { c.receive }.should raise_error
|
29
|
+
c.close
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should not deadlock with multiple threads" do
|
33
|
+
c = Channel.new(:name => "deadlock", :type => String)
|
34
|
+
Thread.new { sleep(0.1); c.push "hi" }
|
35
|
+
lambda { c.receive }.should_not raise_error
|
36
|
+
c.close
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
context "direction" do
|
41
|
+
# A channel provides a mechanism for two concurrently executing functions to
|
42
|
+
# synchronize execution and communicate by passing a value of a specified element
|
43
|
+
# type. The value of an uninitialized channel is nil.
|
44
|
+
|
45
|
+
it "should support send only" do
|
46
|
+
c = Channel.new(:name => "spec", :direction => :send, :type => String, :size => 3)
|
47
|
+
|
48
|
+
lambda { c << "hello" }.should_not raise_error
|
49
|
+
lambda { c.push "hello" }.should_not raise_error
|
50
|
+
lambda { c.send "hello" }.should_not raise_error
|
51
|
+
|
52
|
+
lambda { c.pop }.should raise_error Channel::InvalidDirection
|
53
|
+
lambda { c.receive }.should raise_error Channel::InvalidDirection
|
54
|
+
|
55
|
+
c.close
|
56
|
+
end
|
57
|
+
|
58
|
+
it "should support receive only" do
|
59
|
+
c = Channel.new(:name => "spec", :direction => :receive, :type => String)
|
60
|
+
|
61
|
+
lambda { c << "hello" }.should raise_error Channel::InvalidDirection
|
62
|
+
lambda { c.push "hello" }.should raise_error Channel::InvalidDirection
|
63
|
+
lambda { c.send "hello" }.should raise_error Channel::InvalidDirection
|
64
|
+
|
65
|
+
# timeout blocking receive calls
|
66
|
+
lambda { Timeout::timeout(0.1) { c.pop } }.should raise_error(Timeout::Error)
|
67
|
+
lambda { Timeout::timeout(0.1) { c.receive } }.should raise_error(Timeout::Error)
|
68
|
+
end
|
69
|
+
|
70
|
+
it "should default to bi-directional communication" do
|
71
|
+
lambda { c.send "hello" }.should_not raise_error
|
72
|
+
lambda { c.receive }.should_not raise_error
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
context "typed" do
|
77
|
+
it "should create a typed channel" do
|
78
|
+
lambda { Channel.new(:name => "spec") }.should raise_error Channel::Untyped
|
79
|
+
lambda { Channel.new(:name => "spec", :type => Integer) }.should_not raise_error
|
80
|
+
end
|
81
|
+
|
82
|
+
it "should reject messages of invalid type" do
|
83
|
+
lambda { c.send 1 }.should raise_error(Channel::InvalidType)
|
84
|
+
lambda { c.send "hello" }.should_not raise_error
|
85
|
+
c.receive
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
context "transport" do
|
90
|
+
it "should default to memory transport" do
|
91
|
+
c.transport.should == Agent::Transport::Queue
|
92
|
+
end
|
93
|
+
|
94
|
+
context "channels of channels" do
|
95
|
+
# One of the most important properties of Go is that a channel is a first-class
|
96
|
+
# value that can be allocated and passed around like any other. A common use of
|
97
|
+
# this property is to implement safe, parallel demultiplexing.
|
98
|
+
# - http://golang.org/doc/effective_go.html#chan_of_chan
|
99
|
+
|
100
|
+
it "should be a first class, serializable value" do
|
101
|
+
lambda { Marshal.dump(c) }.should_not raise_error
|
102
|
+
lambda { Marshal.load(Marshal.dump(c)).is_a? Channel }.should_not raise_error
|
103
|
+
end
|
104
|
+
|
105
|
+
it "should be able to pass as a value on a different channel" do
|
106
|
+
c.send "hello"
|
107
|
+
|
108
|
+
cm = Marshal.load(Marshal.dump(c))
|
109
|
+
cm.receive.should == "hello"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
context "capacity" do
|
114
|
+
# The capacity, in number of elements, sets the size of the buffer in the channel.
|
115
|
+
# If the capacity is greater than zero, the channel is asynchronous: provided the
|
116
|
+
# buffer is not full, sends can succeed without blocking. If the capacity is zero
|
117
|
+
# or absent, the communication succeeds only when both a sender and receiver are ready.
|
118
|
+
|
119
|
+
it "should default to synchronous communication" do
|
120
|
+
c = Channel.new(:name => "buffered", :type => String)
|
121
|
+
|
122
|
+
c.send "hello"
|
123
|
+
c.receive.should == "hello"
|
124
|
+
lambda { Timeout::timeout(0.1) { c.receive } }.should raise_error(Timeout::Error)
|
125
|
+
|
126
|
+
c.close
|
127
|
+
end
|
128
|
+
|
129
|
+
it "should support asynchronous communication with buffered capacity" do
|
130
|
+
c = Channel.new(:name => "buffered", :type => String, :size => 2)
|
131
|
+
|
132
|
+
c.send "hello 1"
|
133
|
+
c.send "hello 2"
|
134
|
+
lambda { Timeout::timeout(0.1) { c.send "hello 3" } }.should raise_error(Timeout::Error)
|
135
|
+
|
136
|
+
c.receive.should == "hello 1"
|
137
|
+
c.receive.should == "hello 2"
|
138
|
+
lambda { Timeout::timeout(0.1) { c.receive } }.should raise_error(Timeout::Error)
|
139
|
+
|
140
|
+
c.close
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require "helper"
|
2
|
+
|
3
|
+
describe "Channel of Channels" do
|
4
|
+
|
5
|
+
Request = Struct.new(:args, :resultChan)
|
6
|
+
|
7
|
+
it "should be able to pass channels as first class citizens" do
|
8
|
+
server = Proc.new do |reqs|
|
9
|
+
2.times do |n|
|
10
|
+
res = Request.new(n, Agent::Channel.new(:name => "resultChan-#{n}", :type => Integer))
|
11
|
+
|
12
|
+
reqs << res
|
13
|
+
res.resultChan.receive.should == n+1
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
worker = Proc.new do |reqs|
|
18
|
+
loop do
|
19
|
+
req = reqs.receive
|
20
|
+
req.resultChan << req.args+1
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
clientRequests = Agent::Channel.new(:name => :clientRequests, :type => Request)
|
25
|
+
|
26
|
+
s = go(clientRequests, &server)
|
27
|
+
c = go(clientRequests, &worker)
|
28
|
+
|
29
|
+
s.join
|
30
|
+
clientRequests.close
|
31
|
+
end
|
32
|
+
|
33
|
+
it "should work with multiple workers" do
|
34
|
+
worker = Proc.new do |reqs|
|
35
|
+
loop do
|
36
|
+
req = reqs.receive
|
37
|
+
req.resultChan << req.args+1
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
clientRequests = Agent::Channel.new(:name => :clientRequests, :type => Request)
|
42
|
+
|
43
|
+
# start multiple workers
|
44
|
+
go(clientRequests, &worker)
|
45
|
+
go(clientRequests, &worker)
|
46
|
+
|
47
|
+
# start server
|
48
|
+
s = go clientRequests do |reqs|
|
49
|
+
2.times do |n|
|
50
|
+
res = Request.new(n, Agent::Channel.new(:name => "resultChan-#{n}", :type => Integer))
|
51
|
+
|
52
|
+
reqs << res
|
53
|
+
res.resultChan.receive.should == n+1
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
s.join
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
package main
|
2
|
+
|
3
|
+
import (
|
4
|
+
"fmt"
|
5
|
+
"runtime"
|
6
|
+
)
|
7
|
+
|
8
|
+
func producer(c chan int, N int, s chan bool) {
|
9
|
+
for i := 0; i < N; i++ {
|
10
|
+
fmt.Printf("producer: %d\n", i)
|
11
|
+
c <- i
|
12
|
+
}
|
13
|
+
s <- true
|
14
|
+
}
|
15
|
+
|
16
|
+
func consumer(c chan int, N int, s chan bool) {
|
17
|
+
for i := 0; i < N; i++ {
|
18
|
+
fmt.Printf("consumer got: %d\n", <- c)
|
19
|
+
}
|
20
|
+
s <- true
|
21
|
+
}
|
22
|
+
|
23
|
+
func main() {
|
24
|
+
runtime.GOMAXPROCS(2)
|
25
|
+
|
26
|
+
c := make(chan int)
|
27
|
+
s := make(chan bool)
|
28
|
+
|
29
|
+
go producer(c, 10, s)
|
30
|
+
go consumer(c, 10, s)
|
31
|
+
|
32
|
+
<- s
|
33
|
+
<- s
|
34
|
+
}
|
@@ -0,0 +1,76 @@
|
|
1
|
+
// Copyright 2009 The Go Authors. All rights reserved.
|
2
|
+
// Use of this source code is governed by a BSD-style
|
3
|
+
// license that can be found in the LICENSE file.
|
4
|
+
|
5
|
+
package main
|
6
|
+
|
7
|
+
import (
|
8
|
+
"flag"
|
9
|
+
"fmt"
|
10
|
+
"os"
|
11
|
+
"strconv"
|
12
|
+
)
|
13
|
+
|
14
|
+
var nth = flag.Bool("n", false, "print the nth prime only")
|
15
|
+
|
16
|
+
// Send the sequence 2, 3, 4, ... to returned channel
|
17
|
+
func generate() chan int {
|
18
|
+
ch := make(chan int)
|
19
|
+
go func() {
|
20
|
+
for i := 2; ; i++ {
|
21
|
+
ch <- i
|
22
|
+
}
|
23
|
+
}()
|
24
|
+
return ch
|
25
|
+
}
|
26
|
+
|
27
|
+
// Filter out input values divisible by 'prime', send rest to returned channel
|
28
|
+
func filter(in chan int, prime int) chan int {
|
29
|
+
out := make(chan int)
|
30
|
+
go func() {
|
31
|
+
for {
|
32
|
+
if i := <-in; i%prime != 0 {
|
33
|
+
out <- i
|
34
|
+
}
|
35
|
+
}
|
36
|
+
}()
|
37
|
+
return out
|
38
|
+
}
|
39
|
+
|
40
|
+
func sieve() chan int {
|
41
|
+
out := make(chan int)
|
42
|
+
go func() {
|
43
|
+
ch := generate()
|
44
|
+
for {
|
45
|
+
prime := <-ch
|
46
|
+
out <- prime
|
47
|
+
ch = filter(ch, prime)
|
48
|
+
}
|
49
|
+
}()
|
50
|
+
return out
|
51
|
+
}
|
52
|
+
|
53
|
+
func main() {
|
54
|
+
flag.Parse()
|
55
|
+
n, err := strconv.Atoi(flag.Arg(0))
|
56
|
+
if err != nil {
|
57
|
+
fmt.Fprintf(os.Stderr, "bad argument\n")
|
58
|
+
os.Exit(1)
|
59
|
+
}
|
60
|
+
primes := sieve()
|
61
|
+
if *nth {
|
62
|
+
for i := 1; i < n; i++ {
|
63
|
+
<-primes
|
64
|
+
}
|
65
|
+
fmt.Println(<-primes)
|
66
|
+
} else {
|
67
|
+
for {
|
68
|
+
p := <-primes
|
69
|
+
if p <= n {
|
70
|
+
fmt.Println(p)
|
71
|
+
} else {
|
72
|
+
return
|
73
|
+
}
|
74
|
+
}
|
75
|
+
}
|
76
|
+
}
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require "helper"
|
2
|
+
|
3
|
+
describe "Producer-Consumer" do
|
4
|
+
it "should synchronize by communication" do
|
5
|
+
|
6
|
+
# func producer(c chan int, N int, s chan bool) {
|
7
|
+
# for i := 0; i < N; i++ {
|
8
|
+
# fmt.Printf("producer: %d\n", i)
|
9
|
+
# c <- i
|
10
|
+
# }
|
11
|
+
# s <- true
|
12
|
+
# }
|
13
|
+
#
|
14
|
+
# func consumer(c chan int, N int, s chan bool) {
|
15
|
+
# for i := 0; i < N; i++ {
|
16
|
+
# fmt.Printf("consumer got: %d\n", <- c)
|
17
|
+
# }
|
18
|
+
# s <- true
|
19
|
+
# }
|
20
|
+
#
|
21
|
+
# func main() {
|
22
|
+
# runtime.GOMAXPROCS(2)
|
23
|
+
#
|
24
|
+
# c := make(chan int)
|
25
|
+
# s := make(chan bool)
|
26
|
+
#
|
27
|
+
# go producer(c, 10, s)
|
28
|
+
# go consumer(c, 10, s)
|
29
|
+
#
|
30
|
+
# <- s
|
31
|
+
# <- s
|
32
|
+
# }
|
33
|
+
|
34
|
+
producer = Proc.new do |c, n, s|
|
35
|
+
n.times do |i|
|
36
|
+
c << i
|
37
|
+
# puts "producer: #{i}"
|
38
|
+
end
|
39
|
+
|
40
|
+
s << "producer finished"
|
41
|
+
end
|
42
|
+
|
43
|
+
consumer = Proc.new do |c, n, s|
|
44
|
+
n.times do |i|
|
45
|
+
msg = c.receive
|
46
|
+
# puts "consumer got: #{msg}"
|
47
|
+
end
|
48
|
+
|
49
|
+
s << "consumer finished"
|
50
|
+
end
|
51
|
+
|
52
|
+
c = Agent::Channel.new(name: :c, type: Integer)
|
53
|
+
s = Agent::Channel.new(name: :s, type: String)
|
54
|
+
|
55
|
+
go(c, 3, s, &producer)
|
56
|
+
go(c, 3, s, &consumer)
|
57
|
+
|
58
|
+
s.pop.should == "producer finished"
|
59
|
+
s.pop.should == "consumer finished"
|
60
|
+
|
61
|
+
c.close
|
62
|
+
s.close
|
63
|
+
end
|
64
|
+
|
65
|
+
it "should work as generator" do
|
66
|
+
producer = Proc.new do |c|
|
67
|
+
i = 0
|
68
|
+
loop { c.pipe << i+= 1 }
|
69
|
+
end
|
70
|
+
|
71
|
+
Generator = Struct.new(:name, :pipe)
|
72
|
+
c = Agent::Channel.new(name: :incr, type: Integer)
|
73
|
+
g = Generator.new(:incr, c)
|
74
|
+
|
75
|
+
go(g, &producer)
|
76
|
+
|
77
|
+
c.receive.should == 1
|
78
|
+
c.receive.should == 2
|
79
|
+
|
80
|
+
c.chan.size.should == 0
|
81
|
+
c.receive.should == 3
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
require "helper"
|
2
|
+
|
3
|
+
describe "sieve of Eratosthenes" do
|
4
|
+
|
5
|
+
# http://golang.org/doc/go_tutorial.html#tmp_353
|
6
|
+
|
7
|
+
it "should work using Channel primitives" do
|
8
|
+
|
9
|
+
# send the sequence 2,3,4, ... to returned channel
|
10
|
+
def generate
|
11
|
+
ch = Agent::Channel.new(name: 'generator', type: Integer)
|
12
|
+
|
13
|
+
go do
|
14
|
+
i = 1
|
15
|
+
loop { ch << i+= 1 }
|
16
|
+
end
|
17
|
+
|
18
|
+
return ch
|
19
|
+
end
|
20
|
+
|
21
|
+
# filter out input values divisible by *prime*, send rest to returned channel
|
22
|
+
def filter(in_channel, prime)
|
23
|
+
out = Agent::Channel.new(name: "filter-#{prime}", type: Integer)
|
24
|
+
|
25
|
+
go do
|
26
|
+
loop do
|
27
|
+
i = in_channel.receive
|
28
|
+
out << i if (i % prime) != 0
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
return out
|
33
|
+
end
|
34
|
+
|
35
|
+
def sieve
|
36
|
+
out = Agent::Channel.new(name: 'sieve', type: Integer)
|
37
|
+
|
38
|
+
go do
|
39
|
+
ch = generate
|
40
|
+
loop do
|
41
|
+
prime = ch.receive
|
42
|
+
out << prime
|
43
|
+
ch = filter(ch, prime)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
return out
|
48
|
+
end
|
49
|
+
|
50
|
+
# run the sieve
|
51
|
+
n = 20
|
52
|
+
nth = false
|
53
|
+
|
54
|
+
primes = sieve
|
55
|
+
result = []
|
56
|
+
|
57
|
+
if nth
|
58
|
+
n.times { primes.receive }
|
59
|
+
puts primes.receive
|
60
|
+
else
|
61
|
+
loop do
|
62
|
+
p = primes.receive
|
63
|
+
|
64
|
+
if p <= n
|
65
|
+
result << p
|
66
|
+
else
|
67
|
+
break
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
result.should == [2,3,5,7,11,13,17,19]
|
73
|
+
end
|
74
|
+
|
75
|
+
it "should work with Ruby blocks" do
|
76
|
+
|
77
|
+
# send the sequence 2,3,4, ... to returned channel
|
78
|
+
generate = Proc.new do
|
79
|
+
ch = Agent::Channel.new(name: 'generator-block', type: Integer)
|
80
|
+
|
81
|
+
go do
|
82
|
+
i = 1
|
83
|
+
loop { ch << i+= 1 }
|
84
|
+
end
|
85
|
+
|
86
|
+
ch
|
87
|
+
end
|
88
|
+
|
89
|
+
# filter out input values divisible by *prime*, send rest to returned channel
|
90
|
+
filtr = Proc.new do |in_channel, prime|
|
91
|
+
out = Agent::Channel.new(name: "filter-#{prime}-block", type: Integer)
|
92
|
+
|
93
|
+
go do
|
94
|
+
loop do
|
95
|
+
i = in_channel.receive
|
96
|
+
out << i if (i % prime) != 0
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
out
|
101
|
+
end
|
102
|
+
|
103
|
+
sieve = -> do
|
104
|
+
out = Agent::Channel.new(name: 'sieve-block', type: Integer)
|
105
|
+
|
106
|
+
go do
|
107
|
+
ch = generate.call
|
108
|
+
|
109
|
+
loop do
|
110
|
+
prime = ch.receive
|
111
|
+
out << prime
|
112
|
+
ch = filtr.call(ch, prime)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
out
|
117
|
+
end
|
118
|
+
|
119
|
+
# run the sieve
|
120
|
+
n = 20
|
121
|
+
nth = false
|
122
|
+
|
123
|
+
primes = sieve.call
|
124
|
+
result = []
|
125
|
+
|
126
|
+
if nth
|
127
|
+
n.times { primes.receive }
|
128
|
+
puts primes.receive
|
129
|
+
else
|
130
|
+
loop do
|
131
|
+
p = primes.receive
|
132
|
+
|
133
|
+
if p <= n
|
134
|
+
result << p
|
135
|
+
else
|
136
|
+
break
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
result.should == [2,3,5,7,11,13,17,19]
|
142
|
+
end
|
143
|
+
end
|
data/spec/helper.rb
ADDED
data/spec/queue_spec.rb
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
describe Agent::Transport::Queue do
|
4
|
+
include Agent::Transport
|
5
|
+
|
6
|
+
it "should support synchronous, unbuffered communication" do
|
7
|
+
lambda { Queue.new("spec") }.should_not raise_error
|
8
|
+
|
9
|
+
q = Queue.new("spec")
|
10
|
+
q.max.should == 1
|
11
|
+
q.async?.should be_false
|
12
|
+
|
13
|
+
lambda { q.send("hello") }.should_not raise_error
|
14
|
+
lambda { q.send("hello", true) }.should raise_error(ThreadError, "buffer full")
|
15
|
+
|
16
|
+
q.receive.should == "hello"
|
17
|
+
lambda { q.receive(true) }.should raise_error(ThreadError, "buffer empty")
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should support asynchronous, buffered communication" do
|
21
|
+
lambda { Queue.new("spec", 2) }.should_not raise_error
|
22
|
+
|
23
|
+
q = Queue.new("spec", 2)
|
24
|
+
q.max.should == 2
|
25
|
+
q.async?.should be_true
|
26
|
+
|
27
|
+
lambda { q.send("hello 1") }.should_not raise_error
|
28
|
+
lambda { q.send("hello 2", true) }.should_not raise_error(ThreadError, "buffer full")
|
29
|
+
lambda { q.send("hello 3", true) }.should raise_error(ThreadError, "buffer full")
|
30
|
+
|
31
|
+
q.receive.should == "hello 1"
|
32
|
+
q.receive.should == "hello 2"
|
33
|
+
lambda { q.receive(true) }.should raise_error(ThreadError, "buffer empty")
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should persist data between queue objects" do
|
37
|
+
q = Queue.new("spec")
|
38
|
+
q.send "hello"
|
39
|
+
|
40
|
+
q = Queue.new("spec")
|
41
|
+
q.receive.should == "hello"
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should clear registry on close" do
|
45
|
+
q = Queue.new("spec")
|
46
|
+
q.send "hello"
|
47
|
+
q.close
|
48
|
+
|
49
|
+
q = Queue.new("spec")
|
50
|
+
lambda { q.receive(true) }.should raise_error(ThreadError, "buffer empty")
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
metadata
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: agent
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 1
|
8
|
+
- 0
|
9
|
+
version: 0.1.0
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Ilya Grigorik
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-11-10 00:00:00 -06:00
|
18
|
+
default_executable:
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: rspec
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
segments:
|
29
|
+
- 0
|
30
|
+
version: "0"
|
31
|
+
type: :development
|
32
|
+
version_requirements: *id001
|
33
|
+
description: Agent is a diverse family of related approaches for modelling concurrent systems, in Ruby
|
34
|
+
email:
|
35
|
+
- ilya@igvita.com
|
36
|
+
executables: []
|
37
|
+
|
38
|
+
extensions: []
|
39
|
+
|
40
|
+
extra_rdoc_files: []
|
41
|
+
|
42
|
+
files:
|
43
|
+
- .gitignore
|
44
|
+
- Gemfile
|
45
|
+
- Gemfile.lock
|
46
|
+
- README.md
|
47
|
+
- Rakefile
|
48
|
+
- agent.gemspec
|
49
|
+
- autotest/discover.rb
|
50
|
+
- lib/agent.rb
|
51
|
+
- lib/agent/channel.rb
|
52
|
+
- lib/agent/transport/queue.rb
|
53
|
+
- lib/agent/version.rb
|
54
|
+
- spec/channel_spec.rb
|
55
|
+
- spec/examples/channel_of_channels_spec.rb
|
56
|
+
- spec/examples/go/producer_consumer.go
|
57
|
+
- spec/examples/go/sieve.go
|
58
|
+
- spec/examples/producer_consumer_spec.rb
|
59
|
+
- spec/examples/sieve_spec.rb
|
60
|
+
- spec/helper.rb
|
61
|
+
- spec/queue_spec.rb
|
62
|
+
has_rdoc: true
|
63
|
+
homepage: https://github.com/igrigorik/agent
|
64
|
+
licenses: []
|
65
|
+
|
66
|
+
post_install_message:
|
67
|
+
rdoc_options: []
|
68
|
+
|
69
|
+
require_paths:
|
70
|
+
- lib
|
71
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
72
|
+
none: false
|
73
|
+
requirements:
|
74
|
+
- - ">="
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
segments:
|
77
|
+
- 0
|
78
|
+
version: "0"
|
79
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
80
|
+
none: false
|
81
|
+
requirements:
|
82
|
+
- - ">="
|
83
|
+
- !ruby/object:Gem::Version
|
84
|
+
segments:
|
85
|
+
- 0
|
86
|
+
version: "0"
|
87
|
+
requirements: []
|
88
|
+
|
89
|
+
rubyforge_project: agent
|
90
|
+
rubygems_version: 1.3.7
|
91
|
+
signing_key:
|
92
|
+
specification_version: 3
|
93
|
+
summary: Agent is a diverse family of related approaches for modelling concurrent systems, in Ruby
|
94
|
+
test_files:
|
95
|
+
- spec/channel_spec.rb
|
96
|
+
- spec/examples/channel_of_channels_spec.rb
|
97
|
+
- spec/examples/go/producer_consumer.go
|
98
|
+
- spec/examples/go/sieve.go
|
99
|
+
- spec/examples/producer_consumer_spec.rb
|
100
|
+
- spec/examples/sieve_spec.rb
|
101
|
+
- spec/helper.rb
|
102
|
+
- spec/queue_spec.rb
|