package com.laytonsmith.PureUtilities;
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.Callable;
/**
* This class provides a framework of simple hooks to run critical code on the main thread
* for code that is running off the main thread.
*
* The main operations for code that uses this class are invokeNow and invokeLater, and
* the main operations for code that implements this class are the abstract methods,
* and the configuration methods.
*
*
*/
public abstract class ThreadPump {
private int minHoldTime;
private int maxHoldTime;
private int waitTime;
private Queue<Runnable> eventPump;
private int startStack;
private boolean pumpStarted;
private final String threadName;
private final Object waitForStopLock;
private final Object waitForTaskLock;
private long lockTime;
private long idleTime;
/**
* Creates a new ThreadPump object. All times are in milliseconds.
* @param minHoldTime The minimum amount of time to wait for the next operation
* before giving up control of the main thread again. This should be set to 0 if
* there is no or negligible penalty for regaining the main thread once given
* up.
* @param maxHoldTime The max time that the main thread should be monopolized
* before it is given up. This is useful to prevent starvation.
* @param waitTime The amount of time to wait before resuming if the maxHoldTime
* was met, and thread control was returned.
*/
protected ThreadPump(int minHoldTime, int maxHoldTime, int waitTime, String threadName){
eventPump = new LinkedList<Runnable>();
startStack = 0;
waitForStopLock = new Object();
waitForTaskLock = new Object();
pumpStarted = false;
this.minHoldTime = minHoldTime;
this.maxHoldTime = maxHoldTime;
this.waitTime = waitTime;
this.threadName = threadName;
}
/**
* Starts a transaction. This resets all the mechanisms for wait times.
* For each start() call, there must be exactly one stop() call, however,
* they may be called multiple times, and only when the last stop() is called
* will it actually take effect.
*/
public synchronized void start(){
startStack++;
}
/**
* Stops a transaction, and immediately returns control to the main thread, if
* this is the last stop() method to be called.
*/
public synchronized void stop(){
startStack--;
if(startStack < 0){
throw new RuntimeException("stop() called too many times!");
}
}
/**
* Stops a transaction, but waits for all tasks to complete before returning.
* This should ONLY be called if this is the top level stop, and an exception will
* be thrown if the start stack is not 1.
*/
public void waitForStop() throws InterruptedException {
if(startStack != 1){
throw new RuntimeException("waitForStop called from an inner invocation");
}
synchronized(waitForStopLock){
waitForStopLock.wait();
}
}
/**
* Runs a task on the main thread at some point. This returns
* immediately. Tasks queued up will be run in order, but at some
* indeterminate time in the future.
* @param runnable
*/
public void invokeLater(Runnable runnable){
eventPump.add(runnable);
startPump();
}
/**
* Runs a task on the main thread and waits for it to complete. Tasks
* submitted are queued up in order, so the task may not get run immediately,
* and the task completion time is still dependant on the thread starvation
* parameters.
* @param callable
* @return
*/
public Object invokeNow(final Callable<?> callable) throws InterruptedException {
final Object myLock = new Object();
final Object[] ret = new Object[1];
invokeLater(new Runnable() {
@Override
public void run() {
try{
ret[0] = callable.call();
synchronized(myLock){
myLock.notifyAll();
}
} catch(Exception e){
throw new RuntimeException(e);
}
}
});
synchronized(myLock){
myLock.wait();
}
return ret[0];
}
private void startPump(){
if(!pumpStarted){
synchronized(this){
lockTime = System.currentTimeMillis();
pumpStarted = true;
new Thread(new Runnable(){
@Override
public void run(){
doPump();
}
}, threadName).start();
}
}
synchronized(waitForTaskLock){
waitForTaskLock.notifyAll();
}
}
@SuppressWarnings("SleepWhileInLoop")
private void doPump(){
while(pumpStarted){
runOnMainThread(new Runnable() {
@SuppressWarnings("NestedSynchronizedStatement")
@Override
public void run() {
//This condition happens when we are done with the thread
//AND there are no pending events.
if(eventPump.isEmpty() && startStack == 0){
synchronized(ThreadPump.this){
pumpStarted = false;
return;
}
}
//This condition happens when we need to wait
if(eventPump.isEmpty() && startStack > 0){
long now = System.currentTimeMillis();
//Our wait time is the maximum time left before we reach
//the min hold time.
long waitTime = lockTime + minHoldTime - now;
synchronized(waitForTaskLock){
try {
waitForTaskLock.wait(waitTime);
//Well, no tasks were given, so let's return control
//until further notice.
synchronized(ThreadPump.this){
pumpStarted = false;
return;
}
} catch (InterruptedException ex) {
//We were notified that a new task is present,
//so we'll continue down to the next condition.
}
}
}
if(!eventPump.isEmpty()){
Runnable task = eventPump.poll();
if(task != null){
task.run();
}
//Now check against the max time. If this is greater than that, let's
//return control to the main thread for waitTime.
long now = System.currentTimeMillis();
if(now > lockTime + maxHoldTime){
idleTime = waitTime;
return;
}
}
}
});
if(idleTime > 0){
try {
Thread.sleep(idleTime);
idleTime = 0;
} catch (InterruptedException ex) {
//
}
}
}
}
/**
* Runs a task on the main thread, immediately. This task will be an
* encapsulation of the "blocking calls" that this class provides.
* @param r
*/
protected abstract void runOnMainThread(Runnable r);
}