Proposal: Condition timeouts

Anyway… all I really wanted to do was create a PR to propose a solution for the Condition timeout problem… but git wouldn’t let me… so I have to do it here.

I had a brainstorm this morning about how to handle timeouts in Condition. Why didn’t I (or anyone) think of this before?

I don’t see a downside to it. If the condition is released, any pending timeout routines are stopped and they won’t fire redundant actions. If the condition is not released, timeout routines affect only their own threads (so multiple routines could attach to the same condition with independent timeouts).

Did I miss something? AFAICS this is correct.

EDIT: When you start thinking about multiple threads attaching to the same Condition with different timeout durations, the semantics get tricky. I think this implementation is the easiest to explain: the thread may resume before the timeout expires with success, or time out exactly upon the duration. You could conceive of it as: the Condition expires after a certain time, so the thread may time out earlier than its requested timeout duration – but there will always be some conflicting case (e.g. thread A requests a 1 beat timeout, then thread B requests a 0.1 beat timeout – if A’s takes precedence, then B has to wait longer than expected; if B’s takes precedence, then A expected to allow a whole beat but that would be cut short). So I think per-thread timeouts, while I can imagine arguments against them, are the least confusing way.

Condition {
	var <>test, waitingThreads, waitingTimeouts;

	*new { arg test=false;
		^super.newCopyArgs(test, Array(8))
	}
	wait {
		if (test.value.not, {
			waitingThreads = waitingThreads.add(thisThread.threadPlayer);
			\hang.yield;
		});
	}
	hang { arg value = \hang;
		// ignore the test, just wait
		waitingThreads = waitingThreads.add(thisThread.threadPlayer);
		value.yield;
	}

	setTimeout { |timeout|
		var waitingThread = thisThread.threadPlayer;
		var timeoutThread = Routine {
			timeout.wait;
			waitingThreads.remove(waitingThread);
			waitingTimeouts.remove(timeoutThread);
			waitingThread.clock.sched(0, waitingThread);
		};
		waitingTimeouts = waitingTimeouts.add(timeoutThread);
		timeoutThread.play(thisThread.clock);
	}

	signal {
		var tempWaitingThreads, time;
		if (test.value, {
			waitingTimeouts.do { |thread|
				thread.stop;
			};
			waitingTimeouts = nil;
			time = thisThread.seconds;
			tempWaitingThreads = waitingThreads;
			waitingThreads = nil;
			tempWaitingThreads.do({ arg thread;
				thread.clock.sched(0, thread);
			});
		});
	}
	unhang {
		var tempWaitingThreads, time;
		// ignore the test, just resume all waiting threads
		waitingTimeouts.do { |thread|
			thread.stop;
		};
		waitingTimeouts = nil;
		time = thisThread.seconds;
		tempWaitingThreads = waitingThreads;
		waitingThreads = nil;
		tempWaitingThreads.do({ arg thread;
			thread.clock.sched(0, thread);
		});
	}
}

Here’s a quick test/demo case:

(
fork {
	var cond = Condition.new;
	var result;
	
	thisThread.clock.sched(0.5, {
		if(0.5.coin) {
			result = 1;
			cond.unhang;
		};
	});
	
	cond.setTimeout(1).hang;
	
	if(result.notNil) {
		"Result: %\n".postf(result);
	} {
		"Timed out".postln;
	};
};
)

hjh

Sorry Git is giving you a hard time :frowning: I hope you can create a PR for this at some point.

A few questions:

  • IIUC, with this implementation test could be a Boolean or a Function that returns a Boolean? It look like that’s the case.
  • Shouldn’t waitingTimeouts also be assigned to an Array(8) in *new ?
  • In signal and unhang, you are reseting waitingThreads and waitingTimeouts to nil. Shouldn’t they be reset to an empty Array?

I’m sure the git problem is not too hard; I just ran out of git-fu. At worst, I’ll reclone from my fork and apply patches after that.

That part of the implementation is unchanged from the current Condition. (There’s no need to change it.)

I saw in github there was a discussion of the merits of Boolean or Function here. I tend to think a Function is more expressive. I’m writing on my phone now so I won’t try to write code, but I can send an example later, illustrating why I think so.

I don’t think it’s really necessary.

If you’re creating a Condition, eventually you will hang or wait a thread onto it – so you know that waitingThreads will not go unused. The vast majority of Condition uses in the class library don’t need a timeout, so the chance of putting something into waitingTimeouts is much lower. So: on one hand, we could initialize to nil, and if multiple timeouts are requested (I believe this will be unlikely), GC load would be higher because of array reallocation (but the typical timeout case would add only one item, so reallocation is not likely to be a problem); or OTOH we could preallocate slots for timeouts, knowing that in most cases, the slots will be unused (and in most timeout cases, there would not be multiple threads requesting timeout, so no reallocation) – but there’s a certain minimum GC load which in the large majority of cases would support no actual behavior.

I don’t think it’s necessary to treat both variables identically.

Not necessarily. Calling add on nil to create an array on demand is an idiom that appears elsewhere in the class library.

hjh