All:
Based on some ideas originally by Tom Lieber, here is a Semaphore
class that makes it easy to wait for a specific time OR for an
external signal, whichever comes first. I've been using it
extensively in my real-time performance code and would like to believe
it's well debugged. Presented here in case anyone else might find it
useful.
Comments, questions welcome.
Best,
- Rob Poor
// semaphore.ck -- wait until a specific time arrives or an external
// signal is generated, whichever comes first.
//
// Sample Usage ===================================================
// class TickleSignal extends Object {}
// TickleSignal tickle_signal;
// Semaphore semaphore;
//
// repeat(10) {
// now + 2::second => time test_end_time;
// spork ~ tickler_proc();
// semaphore.wait_for(1::second) @=> Object @ cause;
// <<< "semaphore woke due to", cause.toString() >>>;
// test_end_time => now;
// }
//
// fun void tickler_proc() {
// Std.rand2f(0.0, 2.0)::second => now;
// semaphore.signal(tickle_signal);
// }
// ================================================================
// Author: Robert Poor , November 2009
// TimeoutArrived is used as the <cause> argument for signal() when
// the semaphore wakes due to timeout. While it's not neccessary to
// define a custom class -- any object can be used as the <cause>
// argument to signal() -- a custom class has the nice property that
// .toString() will print out the class name. See Sample Usage above
// to see how a custom class may be used as the argument to signal().
class TimeoutArrived extends Object {};
// ================================================================
// ================================================================
// The Semaphore class supports waiting for an external signal or a
// timeout.
public class Semaphore extends Event {
// ================================================================
// class constants
// TIMEOUT_ARRIVED is a constant returned by wait() to indicate
// that the wait() was terminated by timeout rather than by a user
// signal.
static TimeoutArrived @ TIMEOUT_ARRIVED; new TimeoutArrived @=>
TIMEOUT_ARRIVED;
// ================================================================
// class methods
// ChucK 1.2.1.3 exhibits scheduling bugs when passed time values
// with non-zero fractional parts. truncate_time() removes the
// fractional part to work around the bug. (See set_timeout(t)
// for usage.)
fun static time truncate_time(time t) { return t - (t % samp); }
// ================================================================
// instance variables
Object @ _cause;
time _timeout_time;
// ================================================================
// public instance methods
// ================
// wait() blocks until someone calls signal(cause). It then
// returns the cause argument.
fun Object wait() {
this => now; // wait here for call to signal()
return _cause;
}
// ================
// signal(cause) signals the semaphore, allowing it to return from
// a call to wait() with the given cause.
fun Semaphore signal(Object cause) {
cause @=> _cause; // remember the cause of the signal
clear_timeouts(); // inhibit pending timeouts
this.broadcast(); // unblock the wait()
me.yield(); // allow wait()'ing thread to run
return this;
}
// ================
// set_timeout(time) generates a call to signal(TIMEOUT_ARRIVED) in
// the future. If <time> has already passed, no timeout is set
// and set_timeout() returns null.
fun Semaphore set_timeout(dur duration) { return set_timeout(now
+ duration); }
fun Semaphore set_timeout(time t) {
if ((truncate_time(t) => _timeout_time) <= now) return null;
spork ~ _timeout_proc(t);
me.yield();
return this;
}
// ================
// clear_timeouts() prevents any pending timeouts from generating
// a signal().
//
// Implementation note: each call to set_timeout() spawns a new
// _timeout_proc(), and once spawned, a proc cannot be killed or
// interupted. To inhibit subsequent calls to signal(), we set
// _timeout_time to an "impossible" value (i.e. a time in the
// past) to inhibit the generation of the signals -- see the
// implementation of _timeout_proc() to see why this works.
fun Semaphore clear_timeouts() {
now - 1::second => _timeout_time;
return this;
}
// ================
// wait_for() and wait_until() combine a call to set_timeout()
// followed by a wait(): they will block until the desired time
// arrives OR until <this>.signal() is called from another thread,
// whichever comes first.
fun Object wait_for(dur duration) { return wait_until(now +
duration); }
fun Object wait_until(time t) {
if (set_timeout(t) == null) {
return TIMEOUT_ARRIVED; // don't wait() if time already
passed
} else {
return this.wait();
}
}
// ================================================================
// private methods
// _timeout_proc() is spawned in its own thread at each call to
// set_timeout(). It blocks until the requested time, and if
// the timeout is still current (i.e. now == _timeout_time), it
// calls <this>.signal(TIMEOUT_ARRIVED) to unblock the wait().
fun void _timeout_proc(time t) {
t => now; // wait until the time arrives
// Signal the Semaphore only if this timeout is current
if (now == _timeout_time) this.signal(TIMEOUT_ARRIVED);
}
}