/** Copyright 2015 Tim Engler, Rareventure LLC This file is part of Tiny Travel Tracker. Tiny Travel Tracker is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Tiny Travel Tracker is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Tiny Travel Tracker. If not, see <http://www.gnu.org/licenses/>. */ package com.rareventure.android; import java.util.ArrayList; import java.util.HashSet; import java.util.Set; import android.util.Log; import com.rareventure.gps2.GTG; /** * A super thread is used to share threads between multiple blocking tasks. * This way one thread can handle several operations, each which may need to block. * <p> * It prevents having to create a million different threads for every tiny little operation. * Also, super threads are managed by a SuperThreadManager, which allows them to be shutdown, * paused, resumed, etc. when the corresponding activity goes through these states. *</p> * */ public class SuperThread extends Thread { boolean isSTDead; public SuperThreadManager manager; private Set<Task> tasks = new HashSet<>(); Task currentlyRunningTask; public static abstract class Task { private boolean _runnable = true; //higher has more priority and will always be run first int priority; public SuperThread superThread; long timeToWakeUp = Long.MAX_VALUE; boolean isDead; boolean promisesToDieSoon; /** * Creates a task for the thread. * @param priority higher will run first */ public Task(int priority) { this.priority = priority; } /** * The implementation is expected to do a reasonable amount of work * (considering there are other tasks to run). This method * will be called again and again until stExit() is called. * The task is expected to save its state in field variables * for the next run. * <p> * It may wait on an object by calling stWait() and returning. * doWork() will not be called again until stNotify() is called * on the object. * </p> */ abstract protected void doWork(); /** * specifies what the task is waiting on next before it can do * more work. * <p> Does not need to synchronize on the object to wait on it</p> * <p> * WARNING: this doesn't pause the current task. * The task is expected to exit after calling this method. It * will be recalled when stNotify is called for the given item * </p> * @param time time to wait if none of the objects are stNotified (if zero or less, * will never wake up) * @param item item to wait on, may be null */ protected void stWait(long time, Object item) { if(time > 0) timeToWakeUp = System.currentTimeMillis() + time; if(item != null) superThread.manager.taskWillWaitOn(this, item); } /** * called when the task in finshed and should never take on more work */ protected void stExit() { isDead = true; setRunnable(false); } public void setRunnable(boolean runnable) { synchronized(superThread.manager) { if(runnable != this._runnable) { this._runnable = runnable; superThread.manager.notify(); } } } public void promiseToDieWithoutBadEffect(boolean flag) { synchronized(SuperThread.class) { this.promisesToDieSoon = flag; SuperThread.class.notify(); } } public void abortOrPauseIfNecessary() { superThread.abortOrPauseIfNecessary(); } /** * Notify the listening tasks that they should do work. * Note that even if a task is not waiting, it will restart * immediately after finishing its current work if this * is set (unlike a normal notify which will do nothing * if a thread isn't waiting) * @param o object to notify */ public void stNotify(Object o) { superThread.manager.stNotify(o); } } public SuperThread(SuperThreadManager manager) { manager.addSuperThread(this); } public void addTask(Task task) { synchronized(manager) { task.superThread = this; this.tasks.add(task); manager.notifyAll(); } } public final void run() { for(;;) { //choose a task to run synchronized(manager) { int maxPriority = Integer.MIN_VALUE; Task maxTask = null; long minTimeToWakeUp = Long.MAX_VALUE; long currentTime = System.currentTimeMillis(); ArrayList<Task> tasksToRun = new ArrayList<>(); tasksToRun.addAll(tasks); // Log.d(GTG.TAG,"Super task "+this+" has "+tasksToRun.size()+" tasks"); for(Task t : tasksToRun) { if(t.isDead) tasks.remove(t); else { if(t.timeToWakeUp <= currentTime) { t.setRunnable(true); t.timeToWakeUp = Long.MAX_VALUE; } if(t._runnable && t.priority > maxPriority) { maxTask = t; maxPriority = t.priority; } if(minTimeToWakeUp > t.timeToWakeUp) minTimeToWakeUp = t.timeToWakeUp; } } if(manager.isShutdown) break; //if there is nothing to run at the moment if(maxTask == null ) { try { //wait to be notified (the manager will set one or more tasks to be runnable) //note that minTimeToWakeUp should always be later than currentTime at this point if(minTimeToWakeUp <= currentTime) throw new IllegalStateException("minTimeToWakeUp should always be later than currentTime at this point"); manager.addDeltaToSleepingTheads(1); manager.wait(minTimeToWakeUp- currentTime); manager.addDeltaToSleepingTheads(-1); } catch (InterruptedException e) { break; //if we we're interupted, (due to a shutdown) just quit } continue; } currentlyRunningTask = maxTask; } //we need to run an actual task try { abortOrPauseIfNecessary(); currentlyRunningTask.doWork(); } catch(SIException e) { synchronized(manager) { currentlyRunningTask.isDead = true; break; } } catch(RuntimeException e) { Util.printAllStackTraces(); throw e; } catch(Error e) { Util.printAllStackTraces(); throw e; } synchronized(manager) { currentlyRunningTask = null; } } //tell any thread thats waiting for us all to shutdown that //I have synchronized (manager) { isSTDead = true; for(Task t : tasks) t.isDead = true; manager.notify(); } Log.d(GTG.TAG,"Thread "+this+" exited"); } public static void abortOrPauseIfNecessary() { if(Thread.currentThread() instanceof SuperThread) { SuperThread st = (SuperThread) Thread.currentThread(); synchronized(st.manager) { while(st.manager.isPaused) try { st.manager.wait(); } catch (InterruptedException e) { throw new IllegalStateException(); } if(st.manager.isShutdown) throw new SIException(); } } } /** * Like a regular wait, but throws runtime exception, * siexception if interrupted */ public static void siWait(Object o, long time) { try { if(time == -1) o.wait(); else o.wait(time); } catch(InterruptedException e) { throw new SIException(); } abortOrPauseIfNecessary(); } public void siWait(long time) { siWait(this, time); } public void siWait() { siWait(this, -1); } public void siSleep(long time) { try { sleep(time); } catch(InterruptedException e) { throw new SIException(); } abortOrPauseIfNecessary(); } public static void siWait(Object o) { siWait(o, -1); } public static class SIException extends RuntimeException {} public String toString() { return this.getClass()+ "("+super.toString()+")"; } }