Last active
December 21, 2015 05:18
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. | |
*/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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