concurrent-ruby 0.0.1 → 0.1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 23428b19ba9bf0dbfb21cc1262c8607bd5b40b8d
4
- data.tar.gz: 7757f144dfef3b8823d5dacc6e3b3db6a220a9aa
3
+ metadata.gz: 3f58685d3bea0c67b29987579c607417ba905790
4
+ data.tar.gz: 82ab333727880726e30369350eaca336a81c5f2c
5
5
  SHA512:
6
- metadata.gz: 445c8a14988adb216f4314453381dcc56d27f399444d8cc15f4086151699208a15c7fc14a54abeeccf69bd83440ff3a7d8a7ccf240b1c845d2612d567889ebe4
7
- data.tar.gz: f6ddc0ec2cf80080fafc79be80d7c6d5f7e220685266ff0ccc6964a06b70f54ca3f0ac4b721266510c356c580089dc892efd6a27698b39a3921fd92169a835ee
6
+ metadata.gz: 34d8085f8ca406fd76ecb34d78c84b8faff82f49deb52095c00093f54300beb2a024e5b3dc9bd376aaa317cb7f126bcae2a7a2b87012ef975fa9c4760b23a206
7
+ data.tar.gz: f707b25361f41d930383a4e12381fbd511966a991cb68c5ae97db9fe9494278b78e165bf9f3aadbd3341c9a728c1478d74ae5a8db1482248ce0ed58d34468e33
data/README.md CHANGED
@@ -1,4 +1,224 @@
1
- # Concurrent Ruby
1
+ # Concurrent Ruby [![Build Status](https://secure.travis-ci.org/jdantonio/concurrent-ruby.png)](https://travis-ci.org/jdantonio/concurrent-ruby?branch=master) [![Dependency Status](https://gemnasium.com/jdantonio/concurrent-ruby.png)](https://gemnasium.com/jdantonio/concurrent-ruby)
2
2
 
3
- This gem does nothing. It's a placeholder into which I plan to move the concurrency
4
- features of my [functional-ruby](https://github.com/jdantonio/functional-ruby) gem.
3
+ Erlang, Clojure, and Go inspired concurrent programming tools for Ruby.
4
+
5
+ ## Introduction
6
+
7
+ The old-school "lock and synchronize" approach to concurrency is dead. The future of concurrency
8
+ is asynchronous. Send out a bunch of independent [actors](http://en.wikipedia.org/wiki/Actor_model)
9
+ to do your bidding and process the results when you are ready. Although the idea of the concurrent
10
+ actor originated in the early 1970's it has only recently started catching on. Although there is
11
+ no one "true" actor implementation (what *exactly* is "object oriented," what *exactly* is
12
+ "concurrent programming"), many modern programming languages implement variations on the actor
13
+ theme. This library implements a few of the most interesting and useful of those variations.
14
+
15
+ Remember, *there is no silver bullet in concurrent programming.* Concurrency is hard. Very hard.
16
+ These tools will help ease the burden, but at the end of the day it is essential that you
17
+ *know what you are doing.*
18
+
19
+ The project is hosted on the following sites:
20
+
21
+ * [RubyGems project page](https://rubygems.org/gems/concurrent-ruby)
22
+ * [Source code on GitHub](https://github.com/jdantonio/concurrent-ruby)
23
+ * [YARD documentation on RubyDoc.info](http://rubydoc.info/github/jdantonio/concurrent-ruby/frames)
24
+ * [Continuous integration on Travis-CI](https://travis-ci.org/jdantonio/concurrent-ruby)
25
+ * [Dependency tracking on Gemnasium](https://gemnasium.com/jdantonio/concurrent-ruby)
26
+ * [Follow me on Twitter](https://twitter.com/jerrydantonio)
27
+
28
+ ### Goals
29
+
30
+ My history with high-performance, highly-concurrent programming goes back to my days with C/C++.
31
+ I have the same scars as everyone else doing that kind of work with those languages.
32
+ I'm fascinated by modern concurrency patterns like [Actors](http://en.wikipedia.org/wiki/Actor_model),
33
+ [Agents](http://doc.akka.io/docs/akka/snapshot/java/agents.html), and
34
+ [Promises](http://promises-aplus.github.io/promises-spec/). I'm equally fascinated by languages
35
+ with strong concurrency support like [Erlang](http://www.erlang.org/doc/getting_started/conc_prog.html),
36
+ [Go](http://golang.org/doc/articles/concurrency_patterns.html), and
37
+ [Clojure](http://clojure.org/concurrent_programming). My goal is to implement those patterns in Ruby.
38
+ Specifically:
39
+
40
+ * Stay true to the spirit of the languages providing inspiration
41
+ * But implement in a way that makes sense for Ruby
42
+ * Keep the semantics as idiomatic Ruby as possible
43
+ * Support features that make sense in Ruby
44
+ * Exclude features that don't make sense in Ruby
45
+ * Keep everything small
46
+ * Be as fast as reasonably possible
47
+
48
+ ## Features (and Documentation)
49
+
50
+ Several features from Erlang, Go, Clojure, Java, and JavaScript have been implemented thus far:
51
+
52
+ * Clojure inspired [Agent](https://github.com/jdantonio/concurrent-ruby/blob/master/md/agent.md)
53
+ * EventMachine inspired [Defer](https://github.com/jdantonio/concurrent-ruby/blob/master/md/defer.md)
54
+ * Clojure inspired [Future](https://github.com/jdantonio/concurrent-ruby/blob/master/md/future.md)
55
+ * Go inspired [Goroutine](https://github.com/jdantonio/concurrent-ruby/blob/master/md/goroutine.md)
56
+ * JavaScript inspired [Promise](https://github.com/jdantonio/concurrent-ruby/blob/master/md/promise.md)
57
+ * Java inspired [Thread Pools](https://github.com/jdantonio/concurrent-ruby/blob/master/md/thread_pool.md)
58
+
59
+ ### Is it any good?
60
+
61
+ [Yes](http://news.ycombinator.com/item?id=3067434)
62
+
63
+ ### Supported Ruby versions
64
+
65
+ MRI 1.9.2, 1.9.3, and 2.0. This library is pure Ruby and has no gem dependencies. It should be
66
+ fully compatible with any Ruby interpreter that is 1.9.x compliant. I simply don't know enough
67
+ about JRuby, Rubinius, or the others to fully support them. I can promise good karma and
68
+ attribution on this page to anyone wishing to take responsibility for verifying compaitibility
69
+ with any Ruby other than MRI.
70
+
71
+ ### Install
72
+
73
+ ```shell
74
+ gem install concurrent-ruby
75
+ ```
76
+
77
+ or add the following line to Gemfile:
78
+
79
+ ```ruby
80
+ gem 'concurrent-ruby'
81
+ ```
82
+
83
+ and run `bundle install` from your shell.
84
+
85
+ Once you've installed the gem you must `require` it in your project:
86
+
87
+ ```ruby
88
+ require 'concurrent'
89
+ ```
90
+
91
+ ### Examples
92
+
93
+ For complete examples, see the specific documentation linked above. Below are a few examples to whet your appetite.
94
+
95
+ #### Goroutine (Go)
96
+
97
+ ```ruby
98
+ require 'concurrent'
99
+
100
+ @expected = nil
101
+ go(1, 2, 3){|a, b, c| @expected = [c, b, a] }
102
+ sleep(0.1)
103
+ @expected #=> [3, 2, 1]
104
+ ```
105
+
106
+ #### Agent (Clojure)
107
+
108
+ ```ruby
109
+ require 'concurrent'
110
+
111
+ score = agent(10)
112
+ score.value #=> 10
113
+
114
+ score << proc{|current| current + 100 }
115
+ sleep(0.1)
116
+ score.value #=> 110
117
+
118
+ score << proc{|current| current * 2 }
119
+ sleep(0.1)
120
+ deref score #=> 220
121
+
122
+ score << proc{|current| current - 50 }
123
+ sleep(0.1)
124
+ score.value #=> 170
125
+ ```
126
+
127
+ #### Defer (EventMachine)
128
+
129
+ ```ruby
130
+ require 'concurrent'
131
+
132
+ Concurrent::Defer.new{ "Jerry D'Antonio" }.
133
+ then{|result| puts "Hello, #{result}!" }.
134
+ rescue{|ex| puts ex.message }.
135
+ go
136
+
137
+ #=> Hello, Jerry D'Antonio!
138
+
139
+ operation = proc{ raise StandardError.new('Boom!') }
140
+ callback = proc{|result| puts result }
141
+ errorback = proc{|ex| puts ex.message }
142
+ defer(operation, callback, errorback)
143
+ sleep(0.1)
144
+
145
+ #=> "Boom!"
146
+ ```
147
+
148
+ #### Future (Clojure)
149
+
150
+ ```ruby
151
+ require 'concurrent'
152
+
153
+ count = future{ sleep(1); 10 }
154
+ count.state #=> :pending
155
+ # do stuff...
156
+ count.value #=> 10 (after blocking)
157
+ deref count #=> 10
158
+ ```
159
+
160
+ #### Promise (JavaScript)
161
+
162
+ ```ruby
163
+ require 'concurrent'
164
+
165
+ p = promise("Jerry", "D'Antonio"){|a, b| "#{a} #{b}" }.
166
+ then{|result| "Hello #{result}." }.
167
+ rescue(StandardError){|ex| puts "Boom!" }.
168
+ then{|result| "#{result} Would you like to play a game?"}
169
+ sleep(1)
170
+ p.value #=> "Hello Jerry D'Antonio. Would you like to play a game?"
171
+ ```
172
+
173
+ #### Thread Pools
174
+
175
+ ```ruby
176
+ require 'concurrent'
177
+
178
+ pool = Concurrent::FixedThreadPool.new(10)
179
+ @expected = 0
180
+ pool.post{ sleep(0.5); @expected += 100 }
181
+ pool.post{ sleep(0.5); @expected += 100 }
182
+ pool.post{ sleep(0.5); @expected += 100 }
183
+ @expected #=> nil
184
+ sleep(1)
185
+ @expected #=> 300
186
+
187
+ pool = Concurrent::CachedThreadPool.new
188
+ @expected = 0
189
+ pool << proc{ sleep(0.5); @expected += 10 }
190
+ pool << proc{ sleep(0.5); @expected += 10 }
191
+ pool << proc{ sleep(0.5); @expected += 10 }
192
+ @expected #=> 0
193
+ sleep(1)
194
+ @expected #=> 30
195
+ ```
196
+
197
+ ## Copyright
198
+
199
+ *Concurrent Ruby* is Copyright &copy; 2013 [Jerry D'Antonio](https://twitter.com/jerrydantonio).
200
+ It is free software and may be redistributed under the terms specified in the LICENSE file.
201
+
202
+ ## License
203
+
204
+ Released under the MIT license.
205
+
206
+ http://www.opensource.org/licenses/mit-license.php
207
+
208
+ > Permission is hereby granted, free of charge, to any person obtaining a copy
209
+ > of this software and associated documentation files (the "Software"), to deal
210
+ > in the Software without restriction, including without limitation the rights
211
+ > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
212
+ > copies of the Software, and to permit persons to whom the Software is
213
+ > furnished to do so, subject to the following conditions:
214
+ >
215
+ > The above copyright notice and this permission notice shall be included in
216
+ > all copies or substantial portions of the Software.
217
+ >
218
+ > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
219
+ > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
220
+ > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
221
+ > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
222
+ > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
223
+ > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
224
+ > THE SOFTWARE.
@@ -1 +1,20 @@
1
+ require 'thread'
2
+
1
3
  require 'concurrent/version'
4
+
5
+ require 'concurrent/event'
6
+
7
+ require 'concurrent/agent'
8
+ require 'concurrent/defer'
9
+ require 'concurrent/future'
10
+ require 'concurrent/goroutine'
11
+ require 'concurrent/promise'
12
+ require 'concurrent/obligation'
13
+
14
+ require 'concurrent/thread_pool'
15
+ require 'concurrent/cached_thread_pool'
16
+ require 'concurrent/fixed_thread_pool'
17
+
18
+ require 'concurrent/global_thread_pool'
19
+
20
+ require 'concurrent/event_machine_defer_proxy' if defined?(EventMachine)
@@ -0,0 +1,130 @@
1
+ require 'observer'
2
+ require 'thread'
3
+
4
+ require 'concurrent/global_thread_pool'
5
+
6
+ module Concurrent
7
+
8
+ # An agent is a single atomic value that represents an identity. The current value
9
+ # of the agent can be requested at any time (#deref). Each agent has a work queue and operates on
10
+ # the global thread pool. Consumers can #post code blocks to the agent. The code block (function)
11
+ # will receive the current value of the agent as its sole parameter. The return value of the block
12
+ # will become the new value of the agent. Agents support two error handling modes: fail and continue.
13
+ # A good example of an agent is a shared incrementing counter, such as the score in a video game.
14
+ class Agent
15
+ include Observable
16
+
17
+ TIMEOUT = 5
18
+
19
+ attr_reader :initial
20
+ attr_reader :timeout
21
+
22
+ def initialize(initial, timeout = TIMEOUT)
23
+ @value = initial
24
+ @timeout = timeout
25
+ @rescuers = []
26
+ @validator = nil
27
+ @queue = Queue.new
28
+
29
+ $GLOBAL_THREAD_POOL << proc{ work }
30
+ end
31
+
32
+ def value(timeout = 0) return @value; end
33
+ alias_method :deref, :value
34
+
35
+ def rescue(clazz = Exception, &block)
36
+ @rescuers << Rescuer.new(clazz, block) if block_given?
37
+ return self
38
+ end
39
+ alias_method :catch, :rescue
40
+ alias_method :on_error, :rescue
41
+
42
+ def validate(&block)
43
+ @validator = block if block_given?
44
+ return self
45
+ end
46
+ alias_method :validates, :validate
47
+ alias_method :validate_with, :validate
48
+ alias_method :validates_with, :validate
49
+
50
+ def post(&block)
51
+ return @queue.length unless block_given?
52
+ @queue << block
53
+ return @queue.length
54
+ end
55
+
56
+ def <<(block)
57
+ self.post(&block)
58
+ return self
59
+ end
60
+
61
+ def length
62
+ @queue.length
63
+ end
64
+ alias_method :size, :length
65
+ alias_method :count, :length
66
+
67
+ alias_method :add_watch, :add_observer
68
+
69
+ private
70
+
71
+ # @private
72
+ Rescuer = Struct.new(:clazz, :block)
73
+
74
+ # @private
75
+ def try_rescue(ex) # :nodoc:
76
+ rescuer = @rescuers.find{|r| ex.is_a?(r.clazz) }
77
+ rescuer.block.call(ex) if rescuer
78
+ rescue Exception => e
79
+ # supress
80
+ end
81
+
82
+ # @private
83
+ def work # :nodoc:
84
+ loop do
85
+ Thread.pass
86
+ handler = @queue.pop
87
+ begin
88
+ result = Timeout.timeout(@timeout){
89
+ handler.call(@value)
90
+ }
91
+ if @validator.nil? || @validator.call(result)
92
+ @value = result
93
+ changed
94
+ notify_observers(Time.now, @value)
95
+ end
96
+ rescue Exception => ex
97
+ try_rescue(ex)
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
103
+
104
+ module Kernel
105
+
106
+ def agent(initial, timeout = Concurrent::Agent::TIMEOUT)
107
+ return Concurrent::Agent.new(initial, timeout)
108
+ end
109
+ module_function :agent
110
+
111
+ def deref(agent, timeout = nil)
112
+ if agent.respond_to?(:deref)
113
+ return agent.deref(timeout)
114
+ elsif agent.respond_to?(:value)
115
+ return agent.deref(timeout)
116
+ else
117
+ return nil
118
+ end
119
+ end
120
+ module_function :deref
121
+
122
+ def post(agent, &block)
123
+ if agent.respond_to?(:post)
124
+ return agent.post(&block)
125
+ else
126
+ return nil
127
+ end
128
+ end
129
+ module_function :deref
130
+ end
@@ -0,0 +1,122 @@
1
+ require 'thread'
2
+
3
+ require 'concurrent/thread_pool'
4
+ require 'functional/utilities'
5
+
6
+ module Concurrent
7
+
8
+ def self.new_cached_thread_pool
9
+ return CachedThreadPool.new
10
+ end
11
+
12
+ class CachedThreadPool < ThreadPool
13
+ behavior(:thread_pool)
14
+
15
+ DEFAULT_GC_INTERVAL = 60
16
+ DEFAULT_THREAD_IDLETIME = 60
17
+
18
+ attr_reader :working
19
+
20
+ def initialize(opts = {})
21
+ @gc_interval = opts[:gc_interval] || DEFAULT_GC_INTERVAL
22
+ @thread_idletime = opts[:thread_idletime] || DEFAULT_THREAD_IDLETIME
23
+ super()
24
+ @working = 0
25
+ @mutex = Mutex.new
26
+ end
27
+
28
+ def kill
29
+ @status = :killed
30
+ @mutex.synchronize do
31
+ @pool.each{|t| Thread.kill(t.thread) }
32
+ end
33
+ end
34
+
35
+ def size
36
+ return @pool.length
37
+ end
38
+
39
+ def post(*args, &block)
40
+ raise ArgumentError.new('no block given') unless block_given?
41
+ if running?
42
+ collect_garbage if @pool.empty?
43
+ @mutex.synchronize do
44
+ if @working >= @pool.length
45
+ create_worker_thread
46
+ end
47
+ @queue << [args, block]
48
+ end
49
+ return true
50
+ else
51
+ return false
52
+ end
53
+ end
54
+
55
+ # @private
56
+ def status # :nodoc:
57
+ @mutex.synchronize do
58
+ @pool.collect do |worker|
59
+ [
60
+ worker.status,
61
+ worker.status == :idle ? delta(worker.idletime, timestamp) : nil,
62
+ worker.thread.status
63
+ ]
64
+ end
65
+ end
66
+ end
67
+
68
+ private
69
+
70
+ Worker = Struct.new(:status, :idletime, :thread)
71
+
72
+ # @private
73
+ def create_worker_thread # :nodoc:
74
+ worker = Worker.new(:idle, timestamp, nil)
75
+
76
+ worker.thread = Thread.new(worker) do |me|
77
+
78
+ loop do
79
+ task = @queue.pop
80
+
81
+ @working += 1
82
+ me.status = :working
83
+
84
+ if task == :stop
85
+ me.status = :stopping
86
+ break
87
+ else
88
+ task.last.call(*task.first)
89
+ @working -= 1
90
+ me.status = :idle
91
+ me.idletime = timestamp
92
+ end
93
+ end
94
+
95
+ @pool.delete(me)
96
+ if @pool.empty?
97
+ @termination.set
98
+ @status = :shutdown unless killed?
99
+ end
100
+ end
101
+
102
+ @pool << worker
103
+ end
104
+
105
+ # @private
106
+ def collect_garbage # :nodoc:
107
+ @collector = Thread.new do
108
+ loop do
109
+ sleep(@gc_interval)
110
+ @mutex.synchronize do
111
+ @pool.reject! do |worker|
112
+ worker.thread.status.nil? ||
113
+ (worker.status == :idle && @thread_idletime >= delta(worker.idletime, timestamp))
114
+ end
115
+ end
116
+ @working = @pool.count{|worker| worker.status == :working}
117
+ break if @pool.empty?
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end