seekingalpha_thread 1.0.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.
- checksums.yaml +7 -0
- data/.gitignore +3 -0
- data/.rspec +2 -0
- data/.travis.yml +11 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +13 -0
- data/README.md +229 -0
- data/Rakefile +6 -0
- data/lib/thread/channel.rb +109 -0
- data/lib/thread/delay.rb +94 -0
- data/lib/thread/every.rb +197 -0
- data/lib/thread/future.rb +164 -0
- data/lib/thread/pipe.rb +119 -0
- data/lib/thread/pool.rb +488 -0
- data/lib/thread/process.rb +71 -0
- data/lib/thread/promise.rb +83 -0
- data/lib/thread/recursive_mutex.rb +38 -0
- data/spec/thread/channel_spec.rb +39 -0
- data/spec/thread/delay_spec.rb +11 -0
- data/spec/thread/every_spec.rb +9 -0
- data/spec/thread/future_spec.rb +34 -0
- data/spec/thread/pipe_spec.rb +23 -0
- data/spec/thread/pool_spec.rb +86 -0
- data/spec/thread/promise_spec.rb +35 -0
- data/thread.gemspec +22 -0
- metadata +105 -0
data/lib/thread/every.rb
ADDED
@@ -0,0 +1,197 @@
|
|
1
|
+
#--
|
2
|
+
# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
3
|
+
# Version 2, December 2004
|
4
|
+
#
|
5
|
+
# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
6
|
+
# TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
7
|
+
#
|
8
|
+
# 0. You just DO WHAT THE FUCK YOU WANT TO.
|
9
|
+
#++
|
10
|
+
|
11
|
+
require 'thread'
|
12
|
+
|
13
|
+
# An every runs the given block every given seconds, you can then get the
|
14
|
+
# value, check if the value is old and you can check how many seconds
|
15
|
+
# until the next run.
|
16
|
+
class Thread::Every
|
17
|
+
Cancel = Class.new(Exception)
|
18
|
+
Restart = Class.new(Exception)
|
19
|
+
|
20
|
+
# Create an every with the given seconds and block.
|
21
|
+
def initialize(every, &block)
|
22
|
+
raise ArgumentError, 'no block given' unless block
|
23
|
+
|
24
|
+
@every = every
|
25
|
+
@old = true
|
26
|
+
@mutex = Mutex.new
|
27
|
+
@thread = Thread.new {
|
28
|
+
loop do
|
29
|
+
begin
|
30
|
+
value = block.call
|
31
|
+
|
32
|
+
@mutex.synchronize {
|
33
|
+
@at = Time.now
|
34
|
+
@value = value
|
35
|
+
@old = false
|
36
|
+
@exception = nil
|
37
|
+
}
|
38
|
+
rescue Restart
|
39
|
+
next
|
40
|
+
rescue Exception => e
|
41
|
+
@mutex.synchronize {
|
42
|
+
@at = Time.now
|
43
|
+
@exception = e
|
44
|
+
}
|
45
|
+
|
46
|
+
break if e.is_a? Cancel
|
47
|
+
end
|
48
|
+
|
49
|
+
cond.broadcast if cond?
|
50
|
+
|
51
|
+
begin
|
52
|
+
sleep @every
|
53
|
+
rescue Restart
|
54
|
+
next
|
55
|
+
rescue Cancel => e
|
56
|
+
@mutex.synchronize {
|
57
|
+
@at = Time.now
|
58
|
+
@exception = e
|
59
|
+
}
|
60
|
+
|
61
|
+
break
|
62
|
+
end
|
63
|
+
end
|
64
|
+
}
|
65
|
+
|
66
|
+
ObjectSpace.define_finalizer self, self.class.finalizer(@thread)
|
67
|
+
end
|
68
|
+
|
69
|
+
# @private
|
70
|
+
def self.finalizer(thread)
|
71
|
+
proc {
|
72
|
+
thread.raise Cancel.new
|
73
|
+
}
|
74
|
+
end
|
75
|
+
|
76
|
+
# Change the number of seconds between each call.
|
77
|
+
def every(seconds)
|
78
|
+
@every = seconds
|
79
|
+
|
80
|
+
restart
|
81
|
+
end
|
82
|
+
|
83
|
+
# Cancel the every, {#value} will yield a Cancel exception.
|
84
|
+
def cancel
|
85
|
+
@mutex.synchronize {
|
86
|
+
@thread.raise Cancel.new('every cancelled')
|
87
|
+
}
|
88
|
+
|
89
|
+
self
|
90
|
+
end
|
91
|
+
|
92
|
+
# Check if the every has been cancelled.
|
93
|
+
def cancelled?
|
94
|
+
@mutex.synchronize {
|
95
|
+
@exception.is_a? Cancel
|
96
|
+
}
|
97
|
+
end
|
98
|
+
|
99
|
+
# Checks when the every was cancelled.
|
100
|
+
def cancelled_at
|
101
|
+
if cancelled?
|
102
|
+
@mutex.synchronize {
|
103
|
+
@at
|
104
|
+
}
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# Restart the every.
|
109
|
+
def restart
|
110
|
+
@mutex.synchronize {
|
111
|
+
@thread.raise Restart.new
|
112
|
+
}
|
113
|
+
|
114
|
+
self
|
115
|
+
end
|
116
|
+
|
117
|
+
# Check if the every is running.
|
118
|
+
def running?
|
119
|
+
!cancelled?
|
120
|
+
end
|
121
|
+
|
122
|
+
# Check if the every is old, after the first #value call it becomes old,
|
123
|
+
# until another run of the block is gone)
|
124
|
+
def old?
|
125
|
+
@mutex.synchronize {
|
126
|
+
@old
|
127
|
+
}
|
128
|
+
end
|
129
|
+
|
130
|
+
# Gets the Time when the block was called.
|
131
|
+
def called_at
|
132
|
+
@mutex.synchronize {
|
133
|
+
@at
|
134
|
+
}
|
135
|
+
end
|
136
|
+
|
137
|
+
# Gets how many seconds are missing before another call.
|
138
|
+
def next_in
|
139
|
+
return if cancelled?
|
140
|
+
|
141
|
+
@mutex.synchronize {
|
142
|
+
@every - (Time.now - @at)
|
143
|
+
}
|
144
|
+
end
|
145
|
+
|
146
|
+
# Gets the current every value.
|
147
|
+
def value(timeout = nil)
|
148
|
+
@mutex.synchronize {
|
149
|
+
if @old
|
150
|
+
cond.wait(@mutex, *timeout)
|
151
|
+
end
|
152
|
+
|
153
|
+
@old = true
|
154
|
+
|
155
|
+
if @exception
|
156
|
+
raise @exception
|
157
|
+
else
|
158
|
+
@value
|
159
|
+
end
|
160
|
+
}
|
161
|
+
end
|
162
|
+
|
163
|
+
alias ~ value
|
164
|
+
|
165
|
+
# Gets the current every value, without blocking and waiting for the next
|
166
|
+
# call.
|
167
|
+
def value!
|
168
|
+
@mutex.synchronize {
|
169
|
+
@old = true
|
170
|
+
|
171
|
+
@value unless @exception
|
172
|
+
}
|
173
|
+
end
|
174
|
+
|
175
|
+
private
|
176
|
+
def cond?
|
177
|
+
instance_variable_defined? :@cond
|
178
|
+
end
|
179
|
+
|
180
|
+
def cond
|
181
|
+
@cond ||= ConditionVariable.new
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
class Thread
|
186
|
+
# Helper to create an every
|
187
|
+
def self.every(every, &block)
|
188
|
+
Thread::Every.new(every, &block)
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
module Kernel
|
193
|
+
# Helper to create an every
|
194
|
+
def every(every, &block)
|
195
|
+
Thread::Every.new(every, &block)
|
196
|
+
end
|
197
|
+
end
|
@@ -0,0 +1,164 @@
|
|
1
|
+
#--
|
2
|
+
# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
3
|
+
# Version 2, December 2004
|
4
|
+
#
|
5
|
+
# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
6
|
+
# TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
7
|
+
#
|
8
|
+
# 0. You just DO WHAT THE FUCK YOU WANT TO.
|
9
|
+
#++
|
10
|
+
|
11
|
+
require 'thread'
|
12
|
+
|
13
|
+
# A future is an object that incapsulates a block which is called in a
|
14
|
+
# different thread, upon retrieval the caller gets blocked until the block has
|
15
|
+
# finished running, and its result is returned and cached.
|
16
|
+
class Thread::Future
|
17
|
+
Cancel = Class.new(Exception)
|
18
|
+
|
19
|
+
# Create a future with the passed block and optionally using the passed pool.
|
20
|
+
def initialize(pool = nil, &block)
|
21
|
+
raise ArgumentError, 'no block given' unless block
|
22
|
+
|
23
|
+
@mutex = Mutex.new
|
24
|
+
|
25
|
+
task = proc {
|
26
|
+
begin
|
27
|
+
deliver block.call
|
28
|
+
rescue Exception => e
|
29
|
+
@exception = e
|
30
|
+
|
31
|
+
deliver nil
|
32
|
+
end
|
33
|
+
}
|
34
|
+
|
35
|
+
@thread = pool ? pool.process(&task) : Thread.new(&task)
|
36
|
+
|
37
|
+
ObjectSpace.define_finalizer self, self.class.finalizer(@thread)
|
38
|
+
end
|
39
|
+
|
40
|
+
# @private
|
41
|
+
def self.finalizer(thread)
|
42
|
+
proc {
|
43
|
+
thread.raise Cancel.new
|
44
|
+
}
|
45
|
+
end
|
46
|
+
|
47
|
+
# Check if an exception has been raised.
|
48
|
+
def exception?
|
49
|
+
@mutex.synchronize {
|
50
|
+
instance_variable_defined? :@exception
|
51
|
+
}
|
52
|
+
end
|
53
|
+
|
54
|
+
# Return the raised exception.
|
55
|
+
def exception
|
56
|
+
@mutex.synchronize {
|
57
|
+
@exception
|
58
|
+
}
|
59
|
+
end
|
60
|
+
|
61
|
+
# Check if the future has been called.
|
62
|
+
def delivered?
|
63
|
+
@mutex.synchronize {
|
64
|
+
instance_variable_defined? :@value
|
65
|
+
}
|
66
|
+
end
|
67
|
+
|
68
|
+
alias realized? delivered?
|
69
|
+
|
70
|
+
# Cancel the future, {#value} will yield a Cancel exception
|
71
|
+
def cancel
|
72
|
+
return self if delivered?
|
73
|
+
|
74
|
+
@mutex.synchronize {
|
75
|
+
@thread.raise Cancel.new('future cancelled')
|
76
|
+
}
|
77
|
+
|
78
|
+
self
|
79
|
+
end
|
80
|
+
|
81
|
+
# Check if the future has been cancelled
|
82
|
+
def cancelled?
|
83
|
+
@mutex.synchronize {
|
84
|
+
@exception.is_a? Cancel
|
85
|
+
}
|
86
|
+
end
|
87
|
+
|
88
|
+
# Get the value of the future, if it's not finished running this call will block.
|
89
|
+
#
|
90
|
+
# In case the block raises an exception, it will be raised, the exception is cached
|
91
|
+
# and will be raised every time you access the value.
|
92
|
+
#
|
93
|
+
# An optional timeout can be passed which will return nil if nothing has been
|
94
|
+
# delivered.
|
95
|
+
def value(timeout = nil)
|
96
|
+
raise @exception if exception?
|
97
|
+
|
98
|
+
return @value if delivered?
|
99
|
+
|
100
|
+
@mutex.synchronize {
|
101
|
+
cond.wait(@mutex, *timeout)
|
102
|
+
}
|
103
|
+
|
104
|
+
if exception?
|
105
|
+
raise @exception
|
106
|
+
elsif delivered?
|
107
|
+
return @value
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
alias ~ value
|
112
|
+
|
113
|
+
# Do the same as {#value}, but return nil in case of exception.
|
114
|
+
def value!(timeout = nil)
|
115
|
+
begin
|
116
|
+
value(timeout)
|
117
|
+
rescue Exception
|
118
|
+
nil
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
alias ! value!
|
123
|
+
|
124
|
+
private
|
125
|
+
def cond?
|
126
|
+
instance_variable_defined? :@cond
|
127
|
+
end
|
128
|
+
|
129
|
+
def cond
|
130
|
+
@cond ||= ConditionVariable.new
|
131
|
+
end
|
132
|
+
|
133
|
+
def deliver (value)
|
134
|
+
return if delivered?
|
135
|
+
|
136
|
+
@mutex.synchronize {
|
137
|
+
@value = value
|
138
|
+
|
139
|
+
cond.broadcast if cond?
|
140
|
+
}
|
141
|
+
|
142
|
+
self
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
class Thread
|
147
|
+
# Helper to create a future
|
148
|
+
def self.future(pool = nil, &block)
|
149
|
+
Thread::Future.new(pool, &block)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
module Kernel
|
154
|
+
# Helper to create a future.
|
155
|
+
def future(pool = nil, &block)
|
156
|
+
Thread::Future.new(pool, &block)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
class Thread::Pool
|
161
|
+
def future(&block)
|
162
|
+
Thread.future self, &block
|
163
|
+
end
|
164
|
+
end
|
data/lib/thread/pipe.rb
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
#--
|
2
|
+
# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
3
|
+
# Version 2, December 2004
|
4
|
+
#
|
5
|
+
# DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
6
|
+
# TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
7
|
+
#
|
8
|
+
# 0. You just DO WHAT THE FUCK YOU WANT TO.
|
9
|
+
#++
|
10
|
+
|
11
|
+
require 'thread'
|
12
|
+
|
13
|
+
# A pipe lets you execute various tasks on a set of data in parallel,
|
14
|
+
# each datum inserted in the pipe is passed along through queues to the various
|
15
|
+
# functions composing the pipe, the final result is inserted in the final queue.
|
16
|
+
class Thread::Pipe
|
17
|
+
# A task encapsulates a part of the pipe.
|
18
|
+
class Task
|
19
|
+
attr_accessor :input, :output
|
20
|
+
|
21
|
+
# Create a Task which will call the passed function and get input
|
22
|
+
# from the optional parameter and put output in the optional parameter.
|
23
|
+
def initialize(func, input = Queue.new, output = Queue.new)
|
24
|
+
@input = input
|
25
|
+
@output = output
|
26
|
+
@handling = false
|
27
|
+
|
28
|
+
@thread = Thread.new {
|
29
|
+
while true
|
30
|
+
value = @input.deq
|
31
|
+
|
32
|
+
@handling = true
|
33
|
+
begin
|
34
|
+
value = func.call(value)
|
35
|
+
@output.enq value
|
36
|
+
rescue Exception; end
|
37
|
+
@handling = false
|
38
|
+
end
|
39
|
+
}
|
40
|
+
end
|
41
|
+
|
42
|
+
# Check if the task has nothing to do.
|
43
|
+
def empty?
|
44
|
+
!@handling && @input.empty? && @output.empty?
|
45
|
+
end
|
46
|
+
|
47
|
+
# Stop the task.
|
48
|
+
def kill
|
49
|
+
@thread.raise
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Create a pipe using the optionally passed objects as input and
|
54
|
+
# output queue.
|
55
|
+
#
|
56
|
+
# The objects must respond to #enq and #deq, and block on #deq.
|
57
|
+
def initialize(input = Queue.new, output = Queue.new)
|
58
|
+
@tasks = []
|
59
|
+
|
60
|
+
@input = input
|
61
|
+
@output = output
|
62
|
+
|
63
|
+
ObjectSpace.define_finalizer self, self.class.finalizer(@tasks)
|
64
|
+
end
|
65
|
+
|
66
|
+
# @private
|
67
|
+
def self.finalizer(tasks)
|
68
|
+
proc {
|
69
|
+
tasks.each(&:kill)
|
70
|
+
}
|
71
|
+
end
|
72
|
+
|
73
|
+
# Add a task to the pipe, it must respond to #call and #arity,
|
74
|
+
# and #arity must return 1.
|
75
|
+
def |(func)
|
76
|
+
if func.arity != 1
|
77
|
+
raise ArgumentError, 'wrong arity'
|
78
|
+
end
|
79
|
+
|
80
|
+
Task.new(func, (@tasks.empty? ? @input : Queue.new), @output).tap {|t|
|
81
|
+
@tasks.last.output = t.input unless @tasks.empty?
|
82
|
+
@tasks << t
|
83
|
+
}
|
84
|
+
|
85
|
+
self
|
86
|
+
end
|
87
|
+
|
88
|
+
# Check if the pipe is empty.
|
89
|
+
def empty?
|
90
|
+
@input.empty? && @output.empty? && @tasks.all?(&:empty?)
|
91
|
+
end
|
92
|
+
|
93
|
+
# Insert data in the pipe.
|
94
|
+
def enq(data)
|
95
|
+
return if @tasks.empty?
|
96
|
+
|
97
|
+
@input.enq data
|
98
|
+
|
99
|
+
self
|
100
|
+
end
|
101
|
+
|
102
|
+
alias push enq
|
103
|
+
alias << enq
|
104
|
+
|
105
|
+
# Get an element from the output queue.
|
106
|
+
def deq(non_block = false)
|
107
|
+
@output.deq(non_block)
|
108
|
+
end
|
109
|
+
|
110
|
+
alias pop deq
|
111
|
+
alias ~ deq
|
112
|
+
end
|
113
|
+
|
114
|
+
class Thread
|
115
|
+
# Helper to create a pipe.
|
116
|
+
def self.|(func)
|
117
|
+
Pipe.new | func
|
118
|
+
end
|
119
|
+
end
|