/******************************************************************************* * CogTool Copyright Notice and Distribution Terms * CogTool 1.3, Copyright (c) 2005-2013 Carnegie Mellon University * This software is distributed under the terms of the FSF Lesser * Gnu Public License (see LGPL.txt). * * CogTool is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 2.1 of the License, or * (at your option) any later version. * * CogTool 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 Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with CogTool; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA * * CogTool makes use of several third-party components, with the * following notices: * * Eclipse SWT version 3.448 * Eclipse GEF Draw2D version 3.2.1 * * Unless otherwise indicated, all Content made available by the Eclipse * Foundation is provided to you under the terms and conditions of the Eclipse * Public License Version 1.0 ("EPL"). A copy of the EPL is provided with this * Content and is also available at http://www.eclipse.org/legal/epl-v10.html. * * CLISP version 2.38 * * Copyright (c) Sam Steingold, Bruno Haible 2001-2006 * This software is distributed under the terms of the FSF Gnu Public License. * See COPYRIGHT file in clisp installation folder for more information. * * ACT-R 6.0 * * Copyright (c) 1998-2007 Dan Bothell, Mike Byrne, Christian Lebiere & * John R Anderson. * This software is distributed under the terms of the FSF Lesser * Gnu Public License (see LGPL.txt). * * Apache Jakarta Commons-Lang 2.1 * * This product contains software developed by the Apache Software Foundation * (http://www.apache.org/) * * jopt-simple version 1.0 * * Copyright (c) 2004-2013 Paul R. Holser, Jr. * * 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. * * Mozilla XULRunner 1.9.0.5 * * The contents of this file are subject to the Mozilla Public License * Version 1.1 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at * http://www.mozilla.org/MPL/. * Software distributed under the License is distributed on an "AS IS" * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the * License for the specific language governing rights and limitations * under the License. * * The J2SE(TM) Java Runtime Environment version 5.0 * * Copyright 2009 Sun Microsystems, Inc., 4150 * Network Circle, Santa Clara, California 95054, U.S.A. All * rights reserved. U.S. * See the LICENSE file in the jre folder for more information. ******************************************************************************/ package edu.cmu.cs.hcii.cogtool.util; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import edu.cmu.cs.hcii.cogtool.CogTool; public class ThreadManager { /** * The object representing a thread that the thread manager manages. * Each thread must provide a method indicating the work to be performed * and a method to "clean up" when the thread has completed * (regardless of how it was completed -- that is, even if an * exception is raised or it was canceled [see <code>ICancelable</code>]). * <p> */ public interface IWorkThread { /** * Work to be done by the child thread. */ public void doWork(); /** * Clean-up (if any) to be performed regardless of how the * child thread was terminated. Executed in the child thread. */ public void done(); /** * @return AggregateException, which holds any exceptions thrown in * the work thread */ public AggregateException getWorkExceptions(); /** * If an exception is thrown while running the thread, it should be * added to the AggregateException via this method. */ public void addWorkException(Exception ex); } /** * The entries in the pending work thread queue. */ protected static class WorkThreadEntry { public IWorkThread workThread; public int priority; public WorkThreadEntry(IWorkThread wt, int p) { workThread = wt; priority = p; } } /** * The queue of threads waiting to be executed. * <code>List</code> may be used as a queue from either end; * we push new elements onto the end (using <code>add(wt)</code>) * and pop off the beginning (using <code>remove(0)</code>). * Access is synchronized using the queue object itself. */ protected List<WorkThreadEntry> threadQueue = new ArrayList<WorkThreadEntry>(); /** * The threads currently active. Maps IWorkThread to Thread. * Access is synchronized using the mapping itself */ protected Map<IWorkThread, Thread> activeThreads = new HashMap<IWorkThread, Thread>(); /** * The number of threads that should be active at any one time. * Access is synchronized using the sync flag below. */ public int maxActiveThreads = 3; /** * The synchronization flag to protect <code>maxActiveThreads</code> */ protected Object maxActiveThreadsSync = new Object(); /** * The scheduling thread; created when the worker thread * queue becomes non-empty and terminated when the queue * becomes empty. Limits the number of active threads * according to maxActiveThreads. */ protected class SchedulerThread extends Thread { /** * The scheduler thread tries to empty the queue of pending worker * threads. Once it has succeeded, the scheduler thread exits. */ @Override public void run() { // Loop until the pending worker thread queue is empty. while (true) { // Must wait to dequeue a work thread if too many // other threads are active. if (isOkToStart()) { WorkThreadEntry workThreadEntry = null; // Check if there are any pending work threads synchronized(threadQueue) { if (threadQueue.size() > 0) { workThreadEntry = threadQueue.remove(0); } else { scheduler = null; } } // If no work thread dequeued, the queue must be empty, // so exit this scheduling thread. if (workThreadEntry == null) { return; } // Otherwise, start the work thread. startWorkThread(workThreadEntry.workThread, workThreadEntry.priority); // Wait a little before attempting the next work thread try { sleep(100); } catch (InterruptedException e) { // ignore } } else { // Too many threads are currently executing; wait a while // and try again. try { sleep(1000); } catch (InterruptedException e) { // ignore } } } } // run } /** * Only one scheduler thread is active at a time -- furthermore, * a scheduler thread is active only when there are pending work threads * in the queue. * <p> * Synchronized by threadQueue! */ protected SchedulerThread scheduler = null; /** * The manager is a singleton. */ protected ThreadManager() { } protected static final ThreadManager ONLY = new ThreadManager(); /** * Return the maximum number of active worker threads to be executing. */ public int getMaxThreadCount() { synchronized(maxActiveThreadsSync) { return maxActiveThreads; } } /** * Adjust the maximum number of active threads allowed. * If adjusted down and the number of active threads is too much, * currently nothing is done. * * @param maxThreads initial maximum number of threads allowed * to be active */ public void setMaxThreadCount(int maxThreads) { synchronized(maxActiveThreadsSync) { maxActiveThreads = maxThreads; } } /** * Determine if it is ok to start a work thread based on the * current count of active threads and the max active thread count. */ protected boolean isOkToStart() { int activeThreadCount; int maxThreadCount; synchronized(activeThreads) { activeThreadCount = activeThreads.size(); } synchronized(maxActiveThreadsSync) { maxThreadCount = maxActiveThreads; } return activeThreadCount < maxThreadCount; } /** * Start the given work thread, managing active thread count against * the internal maximum thread count. * * @param workThread the work thread to execute * @param priority the priority at which to execute the thread */ protected void startWorkThread(final IWorkThread workThread, int priority) { // System.out.println("STARTING"); // Create the work thread that performs the thread's work // and always calls <code>done()</code>, regardless whether // the thread terminated normally or via an exception. Thread t = new Thread() { @Override public void run() { try { workThread.doWork(); } catch (Exception e) { if (! CogTool.isBuilt()) { // running under the development environment e.printStackTrace(); } // Can't (shouldn't) propagate exception // to either the scheduler or the main thread, // so simply store the exception for the "caller" // to handle later when the "results" of the // work thread are examined. workThread.addWorkException(e); } finally { // Typical "done" work (especially if using a subclass // of ACogToolWorkThread) should be short; for example, // ACogToolWorkThread schedules the actual "done" // work to be executed on the main UI thread. workThread.done(); synchronized(activeThreads) { activeThreads.remove(workThread); } } } }; // "Manage" the actual thread for the work thread object. synchronized(activeThreads) { activeThreads.put(workThread, t); } // Fork off the thread after setting its priority. t.setPriority(priority); t.start(); } /** * Rationalize the given priority to be between Thread.MIN_PRIORITY * and Thread.MAX_PRIORITY. * * @param priority the given priority * @return the given priority if between Thread.MIN_PRIORITY and * Thread.MAX_PRIORITY; Thread.MIN_PRIORITY if too small and * Thread.MAX_PRIORITY if too large */ protected int prunePriority(int priority) { if (priority < Thread.MIN_PRIORITY) { return Thread.MIN_PRIORITY; } if (priority > Thread.MAX_PRIORITY) { return Thread.MAX_PRIORITY; } return priority; } /** * Add a work thread to the pending thread queue. * If it is ok to start the thread immediately, do so. * Otherwise, if no scheduler is running, generate a new one, which will * attempt to start the given thread when other threads terminate. * <p> * For management purposes, it is currently assumed that a work * thread will be executed at most once at a time. * Uses Thread.NORM_PRIORITY as the priority. * * @param workThread the work thread to manage */ public static void startNewThread(IWorkThread workThread) { ONLY.start(workThread, Thread.NORM_PRIORITY); } /** * Add a work thread to the pending thread queue. * If it is ok to start the thread immediately, do so. * Otherwise, if no scheduler is running, generate a new one, which will * attempt to start the given thread when other threads terminate. * <p> * For management purposes, it is currently assumed that a work * thread will be executed at most once at a time. * * @param workThread the work thread to manage * @param priority the priority the new thread should operate at */ public static void startNewThread(IWorkThread workThread, int priority) { ONLY.start(workThread, priority); } protected void start(IWorkThread workThread, int priority) { if (workThread == null) { throw new IllegalArgumentException("Work thread cannot be null"); } priority = prunePriority(priority); boolean isSchedulerInactive; synchronized(threadQueue) { isSchedulerInactive = (scheduler == null); } if (isSchedulerInactive && isOkToStart()) { startWorkThread(workThread, priority); } else { // System.out.println("QUEUING"); synchronized(threadQueue) { threadQueue.add(new WorkThreadEntry(workThread, priority)); // Scheduler could have gone inactive in the interim! isSchedulerInactive = (scheduler == null); } // Don't need to synchronize this because, if the scheduler is // inactive, the only way to make a new one is through the // main UI thread (i.e., here!). if (isSchedulerInactive) { // System.out.println("NEW SCHEDULER"); scheduler = new SchedulerThread(); scheduler.start(); } } } /** * Adjust the priority of the given work thread; if already executing, * adjust directly; if still pending, reset the associated priority. * If the given workThread cannot be found, no change occurs. * * @param workThread the work thread whose priority to adjust * @param newPriority the new priority */ public void setPriority(IWorkThread workThread, int newPriority) { newPriority = prunePriority(newPriority); boolean notDone = true; synchronized(threadQueue) { Iterator<WorkThreadEntry> pendingWorkThreads = threadQueue.iterator(); while (pendingWorkThreads.hasNext()) { WorkThreadEntry entry = pendingWorkThreads.next(); if (entry.workThread == workThread) { entry.priority = newPriority; notDone = false; break; } } } if (notDone) { Thread workExecutionThread = null; synchronized(activeThreads) { workExecutionThread = activeThreads.get(workThread); } if (workExecutionThread != null) { workExecutionThread.setPriority(newPriority); } } } }