1 | =begin
|
---|
2 |
|
---|
3 | = monitor.rb
|
---|
4 |
|
---|
5 | Copyright (C) 2001 Shugo Maeda <[email protected]>
|
---|
6 |
|
---|
7 | This library is distributed under the terms of the Ruby license.
|
---|
8 | You can freely distribute/modify this library.
|
---|
9 |
|
---|
10 | == example
|
---|
11 |
|
---|
12 | This is a simple example.
|
---|
13 |
|
---|
14 | require 'monitor.rb'
|
---|
15 |
|
---|
16 | buf = []
|
---|
17 | buf.extend(MonitorMixin)
|
---|
18 | empty_cond = buf.new_cond
|
---|
19 |
|
---|
20 | # consumer
|
---|
21 | Thread.start do
|
---|
22 | loop do
|
---|
23 | buf.synchronize do
|
---|
24 | empty_cond.wait_while { buf.empty? }
|
---|
25 | print buf.shift
|
---|
26 | end
|
---|
27 | end
|
---|
28 | end
|
---|
29 |
|
---|
30 | # producer
|
---|
31 | while line = ARGF.gets
|
---|
32 | buf.synchronize do
|
---|
33 | buf.push(line)
|
---|
34 | empty_cond.signal
|
---|
35 | end
|
---|
36 | end
|
---|
37 |
|
---|
38 | The consumer thread waits for the producer thread to push a line
|
---|
39 | to buf while buf.empty?, and the producer thread (main thread)
|
---|
40 | reads a line from ARGF and push it to buf, then call
|
---|
41 | empty_cond.signal.
|
---|
42 |
|
---|
43 | =end
|
---|
44 |
|
---|
45 |
|
---|
46 | #
|
---|
47 | # Adds monitor functionality to an arbitrary object by mixing the module with
|
---|
48 | # +include+. For example:
|
---|
49 | #
|
---|
50 | # require 'monitor.rb'
|
---|
51 | #
|
---|
52 | # buf = []
|
---|
53 | # buf.extend(MonitorMixin)
|
---|
54 | # empty_cond = buf.new_cond
|
---|
55 | #
|
---|
56 | # # consumer
|
---|
57 | # Thread.start do
|
---|
58 | # loop do
|
---|
59 | # buf.synchronize do
|
---|
60 | # empty_cond.wait_while { buf.empty? }
|
---|
61 | # print buf.shift
|
---|
62 | # end
|
---|
63 | # end
|
---|
64 | # end
|
---|
65 | #
|
---|
66 | # # producer
|
---|
67 | # while line = ARGF.gets
|
---|
68 | # buf.synchronize do
|
---|
69 | # buf.push(line)
|
---|
70 | # empty_cond.signal
|
---|
71 | # end
|
---|
72 | # end
|
---|
73 | #
|
---|
74 | # The consumer thread waits for the producer thread to push a line
|
---|
75 | # to buf while buf.empty?, and the producer thread (main thread)
|
---|
76 | # reads a line from ARGF and push it to buf, then call
|
---|
77 | # empty_cond.signal.
|
---|
78 | #
|
---|
79 | module MonitorMixin
|
---|
80 | #
|
---|
81 | # FIXME: This isn't documented in Nutshell.
|
---|
82 | #
|
---|
83 | # Since MonitorMixin.new_cond returns a ConditionVariable, and the example
|
---|
84 | # above calls while_wait and signal, this class should be documented.
|
---|
85 | #
|
---|
86 | class ConditionVariable
|
---|
87 | class Timeout < Exception; end
|
---|
88 |
|
---|
89 | # Create a new timer with the argument timeout, and add the
|
---|
90 | # current thread to the list of waiters. Then the thread is
|
---|
91 | # stopped. It will be resumed when a corresponding #signal
|
---|
92 | # occurs.
|
---|
93 | def wait(timeout = nil)
|
---|
94 | @monitor.instance_eval {mon_check_owner()}
|
---|
95 | timer = create_timer(timeout)
|
---|
96 |
|
---|
97 | Thread.critical = true
|
---|
98 | count = @monitor.instance_eval {mon_exit_for_cond()}
|
---|
99 | @waiters.push(Thread.current)
|
---|
100 |
|
---|
101 | begin
|
---|
102 | Thread.stop
|
---|
103 | return true
|
---|
104 | rescue Timeout
|
---|
105 | return false
|
---|
106 | ensure
|
---|
107 | Thread.critical = true
|
---|
108 | if timer && timer.alive?
|
---|
109 | Thread.kill(timer)
|
---|
110 | end
|
---|
111 | if @waiters.include?(Thread.current) # interrupted?
|
---|
112 | @waiters.delete(Thread.current)
|
---|
113 | end
|
---|
114 | @monitor.instance_eval {mon_enter_for_cond(count)}
|
---|
115 | Thread.critical = false
|
---|
116 | end
|
---|
117 | end
|
---|
118 |
|
---|
119 |
|
---|
120 | # call #wait while the supplied block returns +true+.
|
---|
121 | def wait_while
|
---|
122 | while yield
|
---|
123 | wait
|
---|
124 | end
|
---|
125 | end
|
---|
126 |
|
---|
127 | # call #wait until the supplied block returns +true+.
|
---|
128 | def wait_until
|
---|
129 | until yield
|
---|
130 | wait
|
---|
131 | end
|
---|
132 | end
|
---|
133 |
|
---|
134 | # Wake up and run the next waiter
|
---|
135 | def signal
|
---|
136 | @monitor.instance_eval {mon_check_owner()}
|
---|
137 | Thread.critical = true
|
---|
138 | t = @waiters.shift
|
---|
139 | t.wakeup if t
|
---|
140 | Thread.critical = false
|
---|
141 | Thread.pass
|
---|
142 | end
|
---|
143 |
|
---|
144 | # Wake up all the waiters.
|
---|
145 | def broadcast
|
---|
146 | @monitor.instance_eval {mon_check_owner()}
|
---|
147 | Thread.critical = true
|
---|
148 | for t in @waiters
|
---|
149 | t.wakeup
|
---|
150 | end
|
---|
151 | @waiters.clear
|
---|
152 | Thread.critical = false
|
---|
153 | Thread.pass
|
---|
154 | end
|
---|
155 |
|
---|
156 | def count_waiters
|
---|
157 | return @waiters.length
|
---|
158 | end
|
---|
159 |
|
---|
160 | private
|
---|
161 |
|
---|
162 | def initialize(monitor)
|
---|
163 | @monitor = monitor
|
---|
164 | @waiters = []
|
---|
165 | end
|
---|
166 |
|
---|
167 | def create_timer(timeout)
|
---|
168 | if timeout
|
---|
169 | waiter = Thread.current
|
---|
170 | return Thread.start {
|
---|
171 | Thread.pass
|
---|
172 | sleep(timeout)
|
---|
173 | Thread.critical = true
|
---|
174 | waiter.raise(Timeout.new)
|
---|
175 | }
|
---|
176 | else
|
---|
177 | return nil
|
---|
178 | end
|
---|
179 | end
|
---|
180 | end
|
---|
181 |
|
---|
182 | def self.extend_object(obj)
|
---|
183 | super(obj)
|
---|
184 | obj.instance_eval {mon_initialize()}
|
---|
185 | end
|
---|
186 |
|
---|
187 | #
|
---|
188 | # Attempts to enter exclusive section. Returns +false+ if lock fails.
|
---|
189 | #
|
---|
190 | def mon_try_enter
|
---|
191 | result = false
|
---|
192 | Thread.critical = true
|
---|
193 | if @mon_owner.nil?
|
---|
194 | @mon_owner = Thread.current
|
---|
195 | end
|
---|
196 | if @mon_owner == Thread.current
|
---|
197 | @mon_count += 1
|
---|
198 | result = true
|
---|
199 | end
|
---|
200 | Thread.critical = false
|
---|
201 | return result
|
---|
202 | end
|
---|
203 | # For backward compatibility
|
---|
204 | alias try_mon_enter mon_try_enter
|
---|
205 |
|
---|
206 | #
|
---|
207 | # Enters exclusive section.
|
---|
208 | #
|
---|
209 | def mon_enter
|
---|
210 | Thread.critical = true
|
---|
211 | mon_acquire(@mon_entering_queue)
|
---|
212 | @mon_count += 1
|
---|
213 | Thread.critical = false
|
---|
214 | end
|
---|
215 |
|
---|
216 | #
|
---|
217 | # Leaves exclusive section.
|
---|
218 | #
|
---|
219 | def mon_exit
|
---|
220 | mon_check_owner
|
---|
221 | Thread.critical = true
|
---|
222 | @mon_count -= 1
|
---|
223 | if @mon_count == 0
|
---|
224 | mon_release
|
---|
225 | end
|
---|
226 | Thread.critical = false
|
---|
227 | Thread.pass
|
---|
228 | end
|
---|
229 |
|
---|
230 | #
|
---|
231 | # Enters exclusive section and executes the block. Leaves the exclusive
|
---|
232 | # section automatically when the block exits. See example under
|
---|
233 | # +MonitorMixin+.
|
---|
234 | #
|
---|
235 | def mon_synchronize
|
---|
236 | mon_enter
|
---|
237 | begin
|
---|
238 | yield
|
---|
239 | ensure
|
---|
240 | mon_exit
|
---|
241 | end
|
---|
242 | end
|
---|
243 | alias synchronize mon_synchronize
|
---|
244 |
|
---|
245 | #
|
---|
246 | # FIXME: This isn't documented in Nutshell.
|
---|
247 | #
|
---|
248 | # Create a new condition variable for this monitor.
|
---|
249 | # This facilitates control of the monitor with #signal and #wait.
|
---|
250 | #
|
---|
251 | def new_cond
|
---|
252 | return ConditionVariable.new(self)
|
---|
253 | end
|
---|
254 |
|
---|
255 | private
|
---|
256 |
|
---|
257 | def initialize(*args)
|
---|
258 | super
|
---|
259 | mon_initialize
|
---|
260 | end
|
---|
261 |
|
---|
262 | # called by initialize method to set defaults for instance variables.
|
---|
263 | def mon_initialize
|
---|
264 | @mon_owner = nil
|
---|
265 | @mon_count = 0
|
---|
266 | @mon_entering_queue = []
|
---|
267 | @mon_waiting_queue = []
|
---|
268 | end
|
---|
269 |
|
---|
270 | # Throw a ThreadError exception if the current thread
|
---|
271 | # does't own the monitor
|
---|
272 | def mon_check_owner
|
---|
273 | if @mon_owner != Thread.current
|
---|
274 | raise ThreadError, "current thread not owner"
|
---|
275 | end
|
---|
276 | end
|
---|
277 |
|
---|
278 | def mon_acquire(queue)
|
---|
279 | while @mon_owner && @mon_owner != Thread.current
|
---|
280 | queue.push(Thread.current)
|
---|
281 | Thread.stop
|
---|
282 | Thread.critical = true
|
---|
283 | end
|
---|
284 | @mon_owner = Thread.current
|
---|
285 | end
|
---|
286 |
|
---|
287 | def mon_release
|
---|
288 | @mon_owner = nil
|
---|
289 | t = @mon_waiting_queue.shift
|
---|
290 | t = @mon_entering_queue.shift unless t
|
---|
291 | t.wakeup if t
|
---|
292 | end
|
---|
293 |
|
---|
294 | def mon_enter_for_cond(count)
|
---|
295 | mon_acquire(@mon_waiting_queue)
|
---|
296 | @mon_count = count
|
---|
297 | end
|
---|
298 |
|
---|
299 | def mon_exit_for_cond
|
---|
300 | count = @mon_count
|
---|
301 | @mon_count = 0
|
---|
302 | mon_release
|
---|
303 | return count
|
---|
304 | end
|
---|
305 | end
|
---|
306 |
|
---|
307 | # Monitors provide means of mutual exclusion for Thread programming.
|
---|
308 | # A critical region is created by means of the synchronize method,
|
---|
309 | # which takes a block.
|
---|
310 | # The condition variables (created with #new_cond) may be used
|
---|
311 | # to control the execution of a monitor with #signal and #wait.
|
---|
312 | #
|
---|
313 | # the Monitor class wraps MonitorMixin, and provides aliases
|
---|
314 | # alias try_enter try_mon_enter
|
---|
315 | # alias enter mon_enter
|
---|
316 | # alias exit mon_exit
|
---|
317 | # to access its methods more concisely.
|
---|
318 | class Monitor
|
---|
319 | include MonitorMixin
|
---|
320 | alias try_enter try_mon_enter
|
---|
321 | alias enter mon_enter
|
---|
322 | alias exit mon_exit
|
---|
323 | end
|
---|
324 |
|
---|
325 |
|
---|
326 | # Documentation comments:
|
---|
327 | # - All documentation comes from Nutshell.
|
---|
328 | # - MonitorMixin.new_cond appears in the example, but is not documented in
|
---|
329 | # Nutshell.
|
---|
330 | # - All the internals (internal modules Accessible and Initializable, class
|
---|
331 | # ConditionVariable) appear in RDoc. It might be good to hide them, by
|
---|
332 | # making them private, or marking them :nodoc:, etc.
|
---|
333 | # - The entire example from the RD section at the top is replicated in the RDoc
|
---|
334 | # comment for MonitorMixin. Does the RD section need to remain?
|
---|
335 | # - RDoc doesn't recognise aliases, so we have mon_synchronize documented, but
|
---|
336 | # not synchronize.
|
---|
337 | # - mon_owner is in Nutshell, but appears as an accessor in a separate module
|
---|
338 | # here, so is hard/impossible to RDoc. Some other useful accessors
|
---|
339 | # (mon_count and some queue stuff) are also in this module, and don't appear
|
---|
340 | # directly in the RDoc output.
|
---|
341 | # - in short, it may be worth changing the code layout in this file to make the
|
---|
342 | # documentation easier
|
---|
343 |
|
---|
344 | # Local variables:
|
---|
345 | # mode: Ruby
|
---|
346 | # tab-width: 8
|
---|
347 | # End:
|
---|