Actually on reflection the "free ticket" isn't so hot either since in my earlier proposal the semaphore may be acquired by a test even though the "free ticket" has just been released.
Instead, we should probably just count the tests and release the semaphore eagerly.
Let's see... justing thinking out loud. (probably still room for improvement)
let global-pool-count = DegreeOfParallelism - 1
let global-signal be a monitor object.
function run-test(test)
sort and partition test into parallelizable groups
for each partition in order
run-parallelizable-tests(partition.parallelizable-tests)
run-sequential-tests(partition.sequential-tests)
end
end
function run-sequential-tests(tests)
for each test in tests
run-test(test)
end
end
function run-parallelizable-tests(tests)
let local-running-tests = 0
for each test in tests
enter global-signal monitor
loop
if local-running-tests = 0
then exit loop
end
if global-pool-count = 0
then wait until global-signal is pulsed and restart loop
else decrement global-pool-count and exit loop
end
end
increment local-running-tests
exit global-signal monitor
fork
run-test(test)
enter global-signal monitor
decrement local-running-tests
if local-running-tests = 0
then increment global-pool-count
end
pulse all global-signal
exit global-signal monitor
end
end
join all
end
Note that I wrote the above pseudo-code to use a "monitor" object directly instead of a counting semaphore because of the requirement that we wait until local-running-tests becomes 0 global-pool-count becomes non-zero, whichever comes first.
C# does not provide any language facilities for waiting on multiple conditions simultaneously. Using a single shared monitor object here, while not as efficient as other possibilities (some threads may be woken unnecessarily), is easy to implement correctly.
Jeff.