strand 0.1.0 → 0.2.0.rc0
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/.gitignore +3 -0
- data/CHANGELOG +13 -0
- data/Gemfile +3 -15
- data/LICENSE.txt +1 -0
- data/README.rdoc +12 -19
- data/Rakefile +10 -57
- data/lib/strand.rb +151 -126
- data/lib/strand/atc.rb +1 -1
- data/lib/strand/em/condition_variable.rb +57 -0
- data/lib/strand/em/mutex.rb +63 -0
- data/lib/strand/em/queue.rb +84 -0
- data/lib/strand/em/thread.rb +305 -0
- data/lib/strand/monitor.rb +193 -0
- data/lib/strand/version.rb +3 -0
- data/spec/spec_helper.rb +9 -5
- data/spec/strand/alive.rb +62 -0
- data/spec/strand/condition_variable.rb +10 -0
- data/spec/strand/condition_variable/broadcast.rb +61 -0
- data/spec/strand/condition_variable/signal.rb +62 -0
- data/spec/strand/condition_variable/wait.rb +20 -0
- data/spec/strand/current.rb +15 -0
- data/spec/strand/exit.rb +148 -0
- data/spec/strand/join.rb +60 -0
- data/spec/strand/local_storage.rb +98 -0
- data/spec/strand/mutex.rb +244 -0
- data/spec/strand/pass.rb +9 -0
- data/spec/strand/queue.rb +124 -0
- data/spec/strand/raise.rb +142 -0
- data/spec/strand/run.rb +5 -0
- data/spec/strand/shared.rb +14 -0
- data/spec/strand/sleep.rb +51 -0
- data/spec/strand/status.rb +44 -0
- data/spec/strand/stop.rb +58 -0
- data/spec/strand/strand.rb +32 -0
- data/spec/strand/value.rb +39 -0
- data/spec/strand/wakeup.rb +60 -0
- data/spec/strand_spec.rb +51 -0
- data/spec/support/fixtures.rb +305 -0
- data/spec/support/scratch.rb +17 -0
- data/spec/thread_spec.rb +20 -0
- data/strand.gemspec +23 -0
- metadata +72 -58
- data/Gemfile.lock +0 -40
- data/lib/strand/condition_variable.rb +0 -78
- data/spec/condition_variable_spec.rb +0 -82
- data/test/helper.rb +0 -30
- data/test/test_strand.rb +0 -121
data/lib/strand/atc.rb
CHANGED
@@ -0,0 +1,57 @@
|
|
1
|
+
module Strand
|
2
|
+
module EM
|
3
|
+
# Provides for Strands (Fibers) what Ruby's ConditionVariable provides for Threads.
|
4
|
+
class ConditionVariable
|
5
|
+
|
6
|
+
# Create a new condition variable.
|
7
|
+
def initialize
|
8
|
+
@waiters = []
|
9
|
+
end
|
10
|
+
|
11
|
+
# Using a mutex for condition variables is meant to protect
|
12
|
+
# against race conditions when the signal occurs between testing whether
|
13
|
+
# a wait is needed and waiting. This situation will never occur with
|
14
|
+
# fibers, but the semantic is retained
|
15
|
+
def wait(mutex=nil,timeout = nil)
|
16
|
+
|
17
|
+
if timeout.nil? && (mutex.nil? || Numeric === mutex)
|
18
|
+
timeout = mutex
|
19
|
+
mutex = nil
|
20
|
+
end
|
21
|
+
|
22
|
+
# Get the fiber that called us.
|
23
|
+
strand = Thread.current
|
24
|
+
# Add the fiber to the list of waiters.
|
25
|
+
@waiters << strand
|
26
|
+
begin
|
27
|
+
sleeper = mutex ? mutex : Thread
|
28
|
+
sleeper.sleep(timeout)
|
29
|
+
ensure
|
30
|
+
# Remove from list of waiters.
|
31
|
+
@waiters.delete(strand)
|
32
|
+
end
|
33
|
+
self
|
34
|
+
end
|
35
|
+
|
36
|
+
def signal
|
37
|
+
# If there are no waiters, do nothing.
|
38
|
+
return self if @waiters.empty?
|
39
|
+
|
40
|
+
# Find a waiter to wake up.
|
41
|
+
waiter = @waiters.shift
|
42
|
+
|
43
|
+
# Resume it on next tick.
|
44
|
+
::EM.next_tick{ waiter.wakeup }
|
45
|
+
self
|
46
|
+
end
|
47
|
+
|
48
|
+
def broadcast
|
49
|
+
all_waiting = @waiters.dup
|
50
|
+
@waiters.clear
|
51
|
+
::EM.next_tick { all_waiting.each { |w| w.wakeup } }
|
52
|
+
self
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module Strand
|
2
|
+
module EM
|
3
|
+
class Mutex
|
4
|
+
|
5
|
+
def initialize()
|
6
|
+
@waiters = []
|
7
|
+
end
|
8
|
+
|
9
|
+
def lock()
|
10
|
+
strand = Thread.current
|
11
|
+
@waiters << strand
|
12
|
+
#The lock allows reentry but requires matching unlocks
|
13
|
+
strand.send(:yield_sleep) unless @waiters.first == strand
|
14
|
+
# Now strand has the lock, make sure it is released if the strand dies
|
15
|
+
strand.ensure_hook(self) { release() unless waiters.empty? || waiters.first != strand }
|
16
|
+
self
|
17
|
+
end
|
18
|
+
|
19
|
+
def unlock()
|
20
|
+
strand = Thread.current
|
21
|
+
raise FiberError, "not owner" unless @waiters.first == strand
|
22
|
+
release()
|
23
|
+
end
|
24
|
+
|
25
|
+
def locked?
|
26
|
+
!@waiters.empty? && @waiters.first.alive?
|
27
|
+
end
|
28
|
+
|
29
|
+
def try_lock
|
30
|
+
lock unless locked?
|
31
|
+
end
|
32
|
+
|
33
|
+
def synchronize(&block)
|
34
|
+
lock
|
35
|
+
yield
|
36
|
+
ensure
|
37
|
+
unlock
|
38
|
+
end
|
39
|
+
|
40
|
+
def sleep(timeout=nil)
|
41
|
+
unlock
|
42
|
+
begin
|
43
|
+
Thread.sleep(timeout)
|
44
|
+
ensure
|
45
|
+
lock
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
attr_reader :waiters
|
51
|
+
|
52
|
+
def release()
|
53
|
+
# release the current lock holder, and clear the strand death hook
|
54
|
+
waiters.shift.ensure_hook(self)
|
55
|
+
|
56
|
+
::EM.next_tick do
|
57
|
+
waiters.shift until waiters.empty? || waiters.first.alive?
|
58
|
+
waiters.first.send(:wake_resume) unless waiters.empty?
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
module Strand
|
2
|
+
module EM
|
3
|
+
# A Strand equivalent to ::Queue from thread.rb
|
4
|
+
# queue = Strand::Queue.new
|
5
|
+
#
|
6
|
+
# producer = Strand.new do
|
7
|
+
# 5.times do |i|
|
8
|
+
# Strand.sleep rand(i) # simulate expense
|
9
|
+
# queue << i
|
10
|
+
# puts "#{i} produced"
|
11
|
+
# end
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
# consumer = Strand.new do
|
15
|
+
# 5.times do |i|
|
16
|
+
# value = queue.pop
|
17
|
+
# Strand.sleep rand(i/2) # simulate expense
|
18
|
+
# puts "consumed #{value}"
|
19
|
+
# end
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# consumer.join
|
23
|
+
#
|
24
|
+
class Queue
|
25
|
+
|
26
|
+
# Creates a new queue
|
27
|
+
def initialize
|
28
|
+
@mutex = Mutex.new()
|
29
|
+
@cv = ConditionVariable.new()
|
30
|
+
@q = []
|
31
|
+
@waiting = 0
|
32
|
+
end
|
33
|
+
|
34
|
+
# Pushes +obj+ to the queue
|
35
|
+
def push(obj)
|
36
|
+
@q << obj
|
37
|
+
@mutex.synchronize { @cv.signal }
|
38
|
+
end
|
39
|
+
alias :<< :push
|
40
|
+
alias :enq :push
|
41
|
+
|
42
|
+
# Retrieves data from the queue.
|
43
|
+
#
|
44
|
+
#
|
45
|
+
# If the queue is empty, the calling fiber is suspended until data is
|
46
|
+
# pushed onto the queue, unless +non_block+ is true in which case a
|
47
|
+
# +FiberError+ is raised
|
48
|
+
#
|
49
|
+
def pop(non_block=false)
|
50
|
+
raise FiberError, "queue empty" if non_block && empty?
|
51
|
+
if empty?
|
52
|
+
@waiting += 1
|
53
|
+
@mutex.synchronize { @cv.wait(@mutex) if empty? }
|
54
|
+
@waiting -= 1
|
55
|
+
end
|
56
|
+
# array.pop is like a stack, we're a FIFO
|
57
|
+
@q.shift
|
58
|
+
end
|
59
|
+
alias :shift :pop
|
60
|
+
alias :deq :pop
|
61
|
+
|
62
|
+
# Returns the length of the queue
|
63
|
+
def length
|
64
|
+
@q.length
|
65
|
+
end
|
66
|
+
alias :size :length
|
67
|
+
|
68
|
+
# Returns +true+ if the queue is empty
|
69
|
+
def empty?
|
70
|
+
@q.empty?
|
71
|
+
end
|
72
|
+
|
73
|
+
# Removes all objects from the queue
|
74
|
+
def clear
|
75
|
+
@q.clear
|
76
|
+
end
|
77
|
+
|
78
|
+
# Returns the number of fibers waiting on the queue
|
79
|
+
def num_waiting
|
80
|
+
@waiting
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,305 @@
|
|
1
|
+
require "fiber"
|
2
|
+
require "eventmachine"
|
3
|
+
require 'strand/em/mutex'
|
4
|
+
require 'strand/em/condition_variable'
|
5
|
+
|
6
|
+
module Strand
|
7
|
+
|
8
|
+
module EM
|
9
|
+
|
10
|
+
#Acts like a ::Thread using Fibers and EventMachine
|
11
|
+
class Thread
|
12
|
+
|
13
|
+
@@strands = {}
|
14
|
+
|
15
|
+
# The underlying fiber.
|
16
|
+
attr_reader :fiber
|
17
|
+
|
18
|
+
# Like ::Thread::list. Return an array of all EM::Threads that are alive.
|
19
|
+
def self.list
|
20
|
+
@@strands.values.select { |s| s.alive? }
|
21
|
+
end
|
22
|
+
|
23
|
+
# Like ::Thread::current. Get the currently running EM::Thread, eg to access thread local
|
24
|
+
# variables
|
25
|
+
def self.current
|
26
|
+
@@strands[Fiber.current] || ProxyThread.new(Fiber.current)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Alias for Fiber::yield
|
30
|
+
# Equivalent to a thread being blocked on IO
|
31
|
+
#
|
32
|
+
# WARNING: Be very careful about using #yield with the other thread like methods
|
33
|
+
# Specifically it is important
|
34
|
+
# to ensure user calls to #resume don't conflict with the resumes that are setup via
|
35
|
+
# EM.timer or EM.next_tick as a result of #::sleep or #::pass
|
36
|
+
def self.yield(*args)
|
37
|
+
Fiber.yield(*args)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Like ::Kernel::sleep. Woken by an ::EM::Timer in +seconds+ if supplied
|
41
|
+
def self.sleep(seconds=nil)
|
42
|
+
|
43
|
+
raise TypeError, "seconds #{seconds} must be a number" unless seconds.nil? or seconds.is_a? Numeric
|
44
|
+
n = Time.now
|
45
|
+
|
46
|
+
strand = current
|
47
|
+
timer = ::EM::Timer.new(seconds){ strand.__send__(:wake_resume) } unless seconds.nil?
|
48
|
+
strand.__send__(:yield_sleep,timer)
|
49
|
+
|
50
|
+
(Time.now - n).round()
|
51
|
+
end
|
52
|
+
|
53
|
+
# Like ::Thread::stop. Sleep forever (until woken)
|
54
|
+
def self.stop
|
55
|
+
self.sleep()
|
56
|
+
end
|
57
|
+
|
58
|
+
# Like ::Thread::pass.
|
59
|
+
# The fiber is resumed on the next_tick of EM's event loop
|
60
|
+
def self.pass
|
61
|
+
strand = current
|
62
|
+
::EM.next_tick{ strand.__send__(:wake_resume) }
|
63
|
+
strand.__send__(:yield_sleep)
|
64
|
+
nil
|
65
|
+
end
|
66
|
+
|
67
|
+
# Create and run
|
68
|
+
def initialize(*args,&block)
|
69
|
+
|
70
|
+
# Create our fiber.
|
71
|
+
fiber = Fiber.new{ fiber_body(&block) }
|
72
|
+
|
73
|
+
init(fiber)
|
74
|
+
|
75
|
+
# Finally start the strand.
|
76
|
+
fiber.resume(*args)
|
77
|
+
end
|
78
|
+
|
79
|
+
# Like ::Thread#join.
|
80
|
+
# s1 = Strand.new{ Strand.sleep(1) }
|
81
|
+
# s2 = Strand.new{ Strand.sleep(1) }
|
82
|
+
# s1.join
|
83
|
+
# s2.join
|
84
|
+
def join(limit = nil)
|
85
|
+
@mutex.synchronize { @join_cond.wait(@mutex,limit) } if alive?
|
86
|
+
Kernel.raise @exception if @exception
|
87
|
+
if alive? then nil else self end
|
88
|
+
end
|
89
|
+
|
90
|
+
# Like Fiber#resume. Refer to warnings on #::yield
|
91
|
+
def resume(*args)
|
92
|
+
#TODO should only allow if @status is :run, which really means
|
93
|
+
# blocked by a call to Yield
|
94
|
+
fiber.resume(*args)
|
95
|
+
end
|
96
|
+
|
97
|
+
# Like ::Thread#alive? or Fiber#alive?
|
98
|
+
def alive?
|
99
|
+
fiber.alive?
|
100
|
+
end
|
101
|
+
|
102
|
+
# Like ::Thread#stop? Always true unless our fiber is the current fiber
|
103
|
+
def stop?
|
104
|
+
Fiber.current != fiber
|
105
|
+
end
|
106
|
+
|
107
|
+
# Like ::Thread#status
|
108
|
+
def status
|
109
|
+
case @status
|
110
|
+
when :run
|
111
|
+
#TODO - if not the current fiber
|
112
|
+
# we can only be in this state due to a yield on the
|
113
|
+
# underlying fiber, which means we are actually in sleep
|
114
|
+
# or we're a ProxyThread that is dead and not yet
|
115
|
+
# cleaned up
|
116
|
+
"run"
|
117
|
+
when :sleep
|
118
|
+
"sleep"
|
119
|
+
when :dead, :killed
|
120
|
+
false
|
121
|
+
when :exception
|
122
|
+
nil
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# Like ::Thread#value. Implicitly calls #join.
|
127
|
+
# strand = Strand.new{ 1+2 }
|
128
|
+
# strand.value # => 3
|
129
|
+
def value
|
130
|
+
join and @value
|
131
|
+
end
|
132
|
+
|
133
|
+
# Like ::Thread#exit. Signals thread to wakeup and die
|
134
|
+
def exit
|
135
|
+
case @status
|
136
|
+
when :sleep
|
137
|
+
wake_resume(:exit)
|
138
|
+
when :run
|
139
|
+
throw :exit
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
alias :kill :exit
|
144
|
+
alias :terminate :exit
|
145
|
+
|
146
|
+
# Like ::Thread#wakeup Wakes a sleeping Thread
|
147
|
+
def wakeup
|
148
|
+
Kernel.raise FiberError, "dead strand" unless status
|
149
|
+
wake_resume()
|
150
|
+
end
|
151
|
+
|
152
|
+
# Like ::Thread#raise, raise an exception on a sleeping Thread
|
153
|
+
def raise(*args)
|
154
|
+
if fiber == Fiber.current
|
155
|
+
Kernel.raise *args
|
156
|
+
elsif status
|
157
|
+
args << RuntimeError if args.empty?
|
158
|
+
wake_resume(:raise,*args)
|
159
|
+
else
|
160
|
+
#dead strand, do nothing
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
alias :run :wakeup
|
165
|
+
|
166
|
+
|
167
|
+
# Access to "fiber local" variables, akin to "thread local" variables.
|
168
|
+
# Strand.new do
|
169
|
+
# ...
|
170
|
+
# Strand.current[:connection].send(data)
|
171
|
+
# ...
|
172
|
+
# end
|
173
|
+
def [](name)
|
174
|
+
raise TypeError, "name #{name} must convert to_sym" unless name and name.respond_to?(:to_sym)
|
175
|
+
@locals[name.to_sym]
|
176
|
+
end
|
177
|
+
|
178
|
+
# Access to "fiber local" variables, akin to "thread local" variables.
|
179
|
+
# Strand.new do
|
180
|
+
# ...
|
181
|
+
# Strand.current[:connection] = SomeConnectionClass.new(host, port)
|
182
|
+
# ...
|
183
|
+
# end
|
184
|
+
def []=(name, value)
|
185
|
+
raise TypeError, "name #{name} must convert to_sym" unless name and name.respond_to?(:to_sym)
|
186
|
+
@locals[name.to_sym] = value
|
187
|
+
end
|
188
|
+
|
189
|
+
# Like ::Thread#key? Is there a "fiber local" variable defined called +name+
|
190
|
+
def key?(name)
|
191
|
+
raise TypeError, "name #{name} must convert to_sym" unless name and name.respond_to?(:to_sym)
|
192
|
+
@locals.has_key?(name.to_sym)
|
193
|
+
end
|
194
|
+
|
195
|
+
# Like ::Thread#keys The set of "strand local" variable keys
|
196
|
+
def keys()
|
197
|
+
@locals.keys
|
198
|
+
end
|
199
|
+
|
200
|
+
def inspect #:nodoc:
|
201
|
+
"#<Strand::EM::Thread:0x%s %s" % [object_id, @fiber == Fiber.current ? "run" : "yielded"]
|
202
|
+
end
|
203
|
+
|
204
|
+
# Do something when the fiber completes.
|
205
|
+
def ensure_hook(key,&block)
|
206
|
+
if block_given? then
|
207
|
+
@ensure_hooks[key] = block
|
208
|
+
else
|
209
|
+
@ensure_hooks.delete(key)
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
protected
|
214
|
+
|
215
|
+
def fiber_body(&block) #:nodoc:
|
216
|
+
# Run the strand's block and capture the return value.
|
217
|
+
@status = :run
|
218
|
+
|
219
|
+
@value = nil, @exception = nil
|
220
|
+
catch :exit do
|
221
|
+
begin
|
222
|
+
@value = block.call
|
223
|
+
@status = :dead
|
224
|
+
rescue Exception => e
|
225
|
+
@exception = e
|
226
|
+
@status = :exception
|
227
|
+
ensure
|
228
|
+
run_ensure_hooks()
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
# Delete from the list of running stands.
|
233
|
+
@@strands.delete(@fiber)
|
234
|
+
|
235
|
+
# Resume anyone who called join on us.
|
236
|
+
# the synchronize is not really necessary for fibers
|
237
|
+
# but does no harm
|
238
|
+
@mutex.synchronize { @join_cond.signal() }
|
239
|
+
|
240
|
+
@value || @exception
|
241
|
+
end
|
242
|
+
|
243
|
+
private
|
244
|
+
|
245
|
+
def init(fiber)
|
246
|
+
@fiber = fiber
|
247
|
+
# Add us to the list of living strands.
|
248
|
+
@@strands[@fiber] = self
|
249
|
+
|
250
|
+
# Initialize our "fiber local" storage.
|
251
|
+
@locals = {}
|
252
|
+
|
253
|
+
# Record the status
|
254
|
+
@status = nil
|
255
|
+
|
256
|
+
# Hooks to run when the strand dies (eg by Mutex to release locks)
|
257
|
+
@ensure_hooks = {}
|
258
|
+
|
259
|
+
# Condition variable and mutex for joining.
|
260
|
+
@mutex = Mutex.new()
|
261
|
+
@join_cond = ConditionVariable.new()
|
262
|
+
|
263
|
+
end
|
264
|
+
def yield_sleep(timer=nil)
|
265
|
+
@status = :sleep
|
266
|
+
event,*args = Fiber.yield
|
267
|
+
timer.cancel if timer
|
268
|
+
case event
|
269
|
+
when :exit
|
270
|
+
@status = :killed
|
271
|
+
throw :exit
|
272
|
+
when :wake
|
273
|
+
@status = :run
|
274
|
+
when :raise
|
275
|
+
Kernel.raise *args
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
def wake_resume(event = :wake,*args)
|
280
|
+
fiber.resume(event,*args) if @status == :sleep
|
281
|
+
#TODO if fiber is still alive? and status = :run
|
282
|
+
# then it has been yielded from non Strand code.
|
283
|
+
# if it is not alive, and is a proxy strand then
|
284
|
+
# we can signal the condition variable from here
|
285
|
+
end
|
286
|
+
|
287
|
+
def run_ensure_hooks()
|
288
|
+
#TODO - better not throw exceptions in an ensure hook
|
289
|
+
@ensure_hooks.each { |key,hook| hook.call }
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
# This class is used if EM::Thread class methods are called on Fibers that were not created
|
294
|
+
# with EM::Thread.new()
|
295
|
+
class ProxyThread < Thread
|
296
|
+
|
297
|
+
#TODO start an EM periodic timer to reap dead proxythreads (running ensurehooks)
|
298
|
+
#TODO do something sensible for #value, #kill
|
299
|
+
|
300
|
+
def initialize(fiber)
|
301
|
+
init(fiber)
|
302
|
+
end
|
303
|
+
end
|
304
|
+
end
|
305
|
+
end
|