Skip to content

Instantly share code, notes, and snippets.

@zacscott
Last active December 21, 2015 05:18
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save zacscott/6255315 to your computer and use it in GitHub Desktop.
Save zacscott/6255315 to your computer and use it in GitHub Desktop.
A simple Java utility for a the common run loop thread idiom. You will need some of the other utilities which I have create gists for.
package net.zeddev.util;
import java.util.concurrent.Semaphore;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.zeddev.util.AnnotatedLogger.LogName;
import static net.zeddev.util.Assertions.*;
/**
* <p>
* A thread which executes a run loop. Provides a convenient platform with
* functionality to start and stop the run loop.
* </p>
*
* <p>
* Threads should implement the {@link #runLooped(double) } method which is
* called repeatedly until the thread is stopped. The thread is started as with
* any other with a call to {@link #start()} but can also be safely stopped
* with a call to {@link #stopLoop()}.
* </p>
*
* <p>
* <b>NOTE:</b> This file is released as a stand-alone utility. The original
* file and the associated unit test can be found on GitHub Gist -
* <a href="https://gist.github.com/zscott92/6255315">here</a>.
* </p>
*
* @author Zachary Scott <zscott.dev@gmail.com>
*/
@LogName("RunLoopedThread")
public abstract class RunLoopedThread extends Thread {
public static final Logger logger =
AnnotatedLogger.getLogger(RunLoopedThread.class);
// the semaphore used to signal loop state
private final Semaphore runSignal = new Semaphore(0);
// signal indicating whether loop-round is complete
private Semaphore roundSignal = new Semaphore(1);
// whether the thread loop has been started
private boolean started = false;
/**
* <p>
* Creates a new named {@code RunLoopedThread}.
* </p>
*
* @param name The name of the thread (must not be {@ocde null}).
*/
public RunLoopedThread(String name) {
super(name);
}
/**
* <p>
* Called in the body of the run loop. Called repeatedly until the thread
* is stopped.
* </p>
*
* @param elapsed The time since the last call to this method (in
* seconds).
*/
public abstract void runLooped(double elapsed);
/**
* <p>
* Starts the looped thread. {@code stopLoop()} must be called before
* making additional calls to this method
* </p>
*/
@Override
public synchronized void start() {
require(!started);
logger.log(Level.INFO,
"Starting {0}.",
new Object[] { getName() }
);
runSignal.release();
super.start();
started = true;
}
/**
* <p>
* Stops the thread from executing. MUST be called AFTER a call to
* {@link startLoop()}.
* </p>
*/
public synchronized void stopLoop() {
require(started);
logger.log(Level.INFO,
"Stopping {0}.",
new Object[] { getName() }
);
try {
// release round condition
contRound();
// signal and wait to stop
runSignal.acquire();
started = false;
} catch (InterruptedException ex) {
logger.log(Level.WARNING,
"Failed to stop {0}.",
new Object[] { getName() }
);
// XXX just have to let it go
}
}
/**
* <p>
* Waits for a call to {@link #contRound()} from another thread.
* This mechanism can be used to easily synchronised the {@code runLooped()}
* methods of two or more threads. For example:
* </p>
*
* <pre>
* public void runLooped(double elapsed) {
*
* // do stuff here
* // ...
*
* otherThread.contRound(); // signal other thread that it
* // can start next round
*
* waitRound(); // wait for other thread to signal us
*
* // now we have both finished the body of our runLooped() methods
*
* }
* </pre>
*
* <p>
* <b>NOTE: </b> Unlike the built-in monitor {@code wait()} and
* {@code notify()} methods these methods do NOT have to called from a
* synchronised context. This mechanism is implemented using semaphores
* and as such is inheritly atomic.
* </p>
*/
public void waitRound() {
try {
roundSignal.acquire();
} catch (InterruptedException ex) { }
}
/**
* <p>
* Notifies another thread waiting on a call to {@link #waitRound()}.
* Can be called multiple times without corresponding calls to
* {@link #waitRound()}.
* </p>
*/
public void contRound() {
// release only if currently held
if (roundSignal.availablePermits() == 0)
roundSignal.release();
}
@Override
public void run() {
long startTime = System.currentTimeMillis();
logger.log(Level.INFO,
"{0} started.",
new Object[] { getName() }
);
// the run loop
while (runSignal.tryAcquire()) {
double elapsed = (System.currentTimeMillis() - startTime) / 1000.0f;
// run the loop body
runLooped(elapsed);
// time turn-around on loop
startTime = System.currentTimeMillis();
runSignal.release();
Thread.yield();
}
logger.log(Level.INFO,
"Exiting {0}.",
new Object[] { getName() }
);
}
}
/* Copyright (C) 2013 Zachary Scott <zscott.dev@gmail.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
package net.zeddev.util;
import org.junit.Test;
import static org.junit.Assert.*;
/**
* <p>
* Unit test for {@link RunLoopedThread}.
* </p>
*
* <p>
* <b>NOTE:</b> The {@link RunLoopedThread} class is released as a stand-alone
* utility and the original file can be found on GitHub Gist -
* <a href="https://gist.github.com/zscott92/6255315">here</a>.
* </p>
*
* @author Zachary Scott <zscott.dev@gmail.com>
*/
public class RunLoopedThreadTest {
public RunLoopedThreadTest() {
}
/** Test of {@link RunLoopedThread#runLooped(double)} method. */
@Test
public void testRunLooped() throws Exception {
RunLoopedThreadImpl instance = new RunLoopedThreadImpl();
instance.start();
// check that count changes over time
int count1 = instance.count;
Thread.sleep(100);
int count2 = instance.count;
assertTrue(count2 > 0); // should have at-least changed
assertFalse(count1 == count2);
instance.stopLoop();
}
/** Test of {@link RunLoopedThread#start()} and
* {@link RunLoopedThread#stopLoop()} methods.
*/
@Test
public void testStartStop() throws Exception {
RunLoopedThreadImpl instance = new RunLoopedThreadImpl();
instance.start();
Thread.sleep(100);
instance.stopLoop(); // should be stopped by the time this ends
int countWhenStopped = instance.count;
// check that loop has stopped by checking count has not changed
Thread.sleep(100);
assertEquals(countWhenStopped, instance.count);
}
/** Test of {@link RunLoopedThread#start()} method.
*/
@Test
public void testWaitStart() throws Exception {
RunLoopedThreadImpl instance = new RunLoopedThreadImpl() {
public void runLooped(double elapsed) {
super.runLooped(elapsed); // NOTE will execute first
waitRound(); // wait for signal first
}
};
assertEquals(instance.count, 0);
instance.start();
// step the instance
instance.contRound();
Thread.sleep(100);
// should have incremented exactly once and then waited
assertEquals(instance.count, 2);
// try again
instance.contRound();
Thread.sleep(100);
assertEquals(instance.count, 3);
// then stop and double check
instance.stopLoop();
Thread.sleep(100);
// should nopt do round again as stopped
assertEquals(instance.count, 3);
}
public class RunLoopedThreadImpl extends RunLoopedThread {
public int count = 0;
public RunLoopedThreadImpl() {
super("test RunLoopedThread");
}
@Override
public void runLooped(double elapsed) {
count++;
}
}
}
/* Copyright (C) 2013 Zachary Scott <zscott.dev@gmail.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
* IN THE SOFTWARE.
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment