package org.threadly.concurrent; import java.util.List; import java.util.Queue; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.LockSupport; import org.threadly.util.ArgumentVerifier; import org.threadly.util.Clock; import org.threadly.util.ExceptionHandler; import org.threadly.util.ExceptionUtils; /** * Executor which has no threads itself. This allows you to have the same scheduler abilities * (schedule tasks, recurring tasks, etc, etc), without having to deal with multiple threads, * memory barriers, or other similar concerns. This class can be very useful in GUI development * (if you want it to run on the GUI thread). It also can be useful in android development in a * very similar way. * <p> * The tasks in this scheduler are only progressed forward with calls to * {@link #tick(ExceptionHandler)}. Since it is running on the calling thread, calls to * {@code Object.wait()} and {@code Thread.sleep()} from sub tasks will block (possibly forever). * The call to {@link #tick(ExceptionHandler)} will not unblock till there is no more work for the * scheduler to currently handle. * * @since 2.0.0 */ public class NoThreadScheduler extends AbstractPriorityScheduler { protected final QueueSetListener queueListener; protected final QueueManager queueManager; protected final AtomicReference<Thread> blockingThread; private volatile boolean tickRunning; private volatile boolean tickCanceled; /** * Constructs a new {@link NoThreadScheduler} scheduler. */ public NoThreadScheduler() { this(null, DEFAULT_LOW_PRIORITY_MAX_WAIT_IN_MS); } /** * Constructs a new {@link NoThreadScheduler} scheduler with specified default priority behavior. * * @param defaultPriority Default priority for tasks which are submitted without any specified priority * @param maxWaitForLowPriorityInMs time low priority tasks to wait if there are high priority tasks ready to run */ public NoThreadScheduler(TaskPriority defaultPriority, long maxWaitForLowPriorityInMs) { super(defaultPriority); queueManager = new QueueManager(queueListener = new QueueSetListener() { @Override public void handleQueueUpdate() { Thread t = blockingThread.get(); if (t != null) { LockSupport.unpark(t); } } }, maxWaitForLowPriorityInMs); blockingThread = new AtomicReference<>(null); tickRunning = false; tickCanceled = false; // call to verify and set values setMaxWaitForLowPriority(maxWaitForLowPriorityInMs); } /** * Abstract call to get the value the scheduler should use to represent the current time. This * can be overridden if someone wanted to artificially change the time. * * @param accurate If {@code true} then time estimates are not acceptable * @return current time in milliseconds */ protected long nowInMillis(boolean accurate) { if (accurate) { return Clock.accurateForwardProgressingMillis(); } else { return Clock.lastKnownForwardProgressingMillis(); } } /** * Call to cancel current or the next tick call. If currently in a * {@link #tick(ExceptionHandler)} call (weather blocking waiting for tasks, or currently running * tasks), this will call the {@link #tick(ExceptionHandler)} to return. If a task is currently * running it will finish the current task before returning. If not currently in a * {@link #tick(ExceptionHandler)} call, the next tick call will return immediately without * running anything. */ public void cancelTick() { tickCanceled = true; queueListener.handleQueueUpdate(); } /** * Invoking this will run any tasks which are ready to be run. This will block as it runs as * many scheduled or waiting tasks as possible. It is CRITICAL that only one thread at a time * calls the {@link #tick(ExceptionHandler)} OR {@link #blockingTick(ExceptionHandler)}. While * this class is in general thread safe, if multiple threads invoke either function at the same * time, it is possible a given task may run more than once. In order to maintain high * performance, threadly does not guard against this condition. * <p> * This call allows you to specify an {@link ExceptionHandler}. If provided, if any tasks throw * an exception, this will be called to communicate the exception. This allows you to ensure * that you get a returned task count (meaning if provided, no exceptions will be thrown from * this invocation). If {@code null} is provided for the exception handler, than any tasks * which throw a {@link RuntimeException}, will throw out of this invocation. * <p> * This call is NOT thread safe, calling {@link #tick(ExceptionHandler)} or * {@link #blockingTick(ExceptionHandler)} in parallel could cause the same task to be * run multiple times in parallel. Invoking in parallel will also make the behavior of * {@link #getActiveTaskCount()} non-deterministic and inaccurate. * * @since 3.2.0 * @param exceptionHandler Exception handler implementation to call if any tasks throw an * exception, or null to have exceptions thrown out of this call * @return quantity of tasks run during this tick invocation */ public int tick(ExceptionHandler exceptionHandler) { return tick(exceptionHandler, true); } /** * Internal tick implementation. Allowing control on if the cancelTick boolean should be reset * if no tasks are run. Thus allowing for an optimistic attempt to run tasks, while maintaining * the cancelTick state. * * @param exceptionHandler Exception handler implementation to call if any tasks throw an * exception, or null to have exceptions thrown out of this call * @param resetCancelTickIfNoTasksRan if {@code true} will reset cancelTick weather tasks ran or * not, otherwise cancelTick will only be reset if tasks ran * @return quantity of tasks run during this tick invocation */ private int tick(ExceptionHandler exceptionHandler, boolean resetCancelTickIfNoTasksRan) { int tasks = 0; TaskWrapper nextTask; tickRunning = true; try { while ((nextTask = getNextReadyTask()) != null && ! tickCanceled) { // call will remove task from queue, or reposition as necessary // we can cheat with the execution reference since task de-queue is single threaded if (nextTask.canExecute(nextTask.getExecuteReference())) { try { nextTask.runTask(); } catch (Throwable t) { if (exceptionHandler != null) { exceptionHandler.handleException(t); } else { throw ExceptionUtils.makeRuntime(t); } } tasks++; } } if ((tasks != 0 || resetCancelTickIfNoTasksRan) && tickCanceled) { // reset for future tick calls tickCanceled = false; } return tasks; } finally { tickRunning = false; } } /** * This is similar to {@link #tick(ExceptionHandler)}, except that it will block until there are * tasks ready to run, or until {@link #cancelTick()} is invoked. * <p> * Once there are tasks ready to run, this will continue to block as it runs as many tasks that * are ready to run. * <p> * It is CRITICAL that only one thread at a time calls the {@link #tick(ExceptionHandler)} OR * {@link #blockingTick(ExceptionHandler)}. * <p> * This call allows you to specify an {@link ExceptionHandler}. If provided, if any tasks throw * an exception, this will be called to communicate the exception. This allows you to ensure * that you get a returned task count (meaning if provided, no exceptions will be thrown from * this invocation). If {@code null} is provided for the exception handler, than any tasks * which throw a {@link RuntimeException}, will throw out of this invocation. * <p> * This call is NOT thread safe, calling {@link #tick(ExceptionHandler)} or * {@link #blockingTick(ExceptionHandler)} in parallel could cause the same task to be * run multiple times in parallel. Invoking in parallel will also make the behavior of * {@link #getActiveTaskCount()} non-deterministic and inaccurate. * * @since 4.0.0 * @param exceptionHandler Exception handler implementation to call if any tasks throw an * exception, or null to have exceptions thrown out of this call * @return quantity of tasks run during this tick invocation * @throws InterruptedException thrown if thread is interrupted waiting for task to run */ public int blockingTick(ExceptionHandler exceptionHandler) throws InterruptedException { int initialTickResult = tick(exceptionHandler, false); if (initialTickResult == 0) { Thread currentThread = Thread.currentThread(); // we already tried to optimistically run something above, so we now must prepare to park // and park if we still find nothing to execute if (! blockingThread.compareAndSet(null, currentThread)) { throw new IllegalStateException("Another thread is already blocking!!"); } try { while (true) { /* we must check the cancelTick once we have the lock * since that is when the .notify() would happen. */ if (tickCanceled) { tickCanceled = false; return 0; } else if (currentThread.isInterrupted()) { throw new InterruptedException(); } TaskWrapper nextTask = queueManager.getNextTask(); if (nextTask == null) { LockSupport.park(); } else { long nextTaskDelay = nextTask.getScheduleDelay(); if (nextTaskDelay > 0) { LockSupport.parkNanos(Clock.NANOS_IN_MILLISECOND * nextTaskDelay); } else { // task is ready to run, so break loop break; } } } } finally { // lazy set should be safe here as a CAS operation (the only read done for this atomic) // should invoke a volatile write to see the update. But assuming it does not (since the // docs are not perfectly clear on it), in theory only one thread should be invoking tick // anyways, so the worst case would be another thread sees us as still blocking even // though we have gone into tick (or completed), but that would be an indication of a bad // design pattern with NoThreadScheduler. If only one thread is invoking tick (as it // should). This would never be seen. blockingThread.lazySet(null); } return tick(exceptionHandler, true); } else { return initialTickResult; } } @Override protected OneTimeTaskWrapper doSchedule(Runnable task, long delayInMillis, TaskPriority priority) { QueueSet queueSet = queueManager.getQueueSet(priority); OneTimeTaskWrapper result; if (delayInMillis == 0) { queueSet.addExecute((result = new NoThreadOneTimeTaskWrapper(task, queueSet.executeQueue, nowInMillis(false)))); } else { queueSet.addScheduled((result = new NoThreadOneTimeTaskWrapper(task, queueSet.scheduleQueue, nowInMillis(true) + delayInMillis))); } return result; } @Override public void scheduleWithFixedDelay(Runnable task, long initialDelay, long recurringDelay, TaskPriority priority) { ArgumentVerifier.assertNotNull(task, "task"); ArgumentVerifier.assertNotNegative(initialDelay, "initialDelay"); ArgumentVerifier.assertNotNegative(recurringDelay, "recurringDelay"); if (priority == null) { priority = defaultPriority; } QueueSet queueSet = queueManager.getQueueSet(priority); NoThreadRecurringDelayTaskWrapper taskWrapper = new NoThreadRecurringDelayTaskWrapper(task, queueSet, nowInMillis(true) + initialDelay, recurringDelay); queueSet.addScheduled(taskWrapper); } @Override public void scheduleAtFixedRate(Runnable task, long initialDelay, long period, TaskPriority priority) { ArgumentVerifier.assertNotNull(task, "task"); ArgumentVerifier.assertNotNegative(initialDelay, "initialDelay"); ArgumentVerifier.assertGreaterThanZero(period, "period"); if (priority == null) { priority = defaultPriority; } QueueSet queueSet = queueManager.getQueueSet(priority); NoThreadRecurringRateTaskWrapper taskWrapper = new NoThreadRecurringRateTaskWrapper(task, queueSet, nowInMillis(true) + initialDelay, period); queueSet.addScheduled(taskWrapper); } @Override public int getActiveTaskCount() { return tickRunning ? 1 : 0; } @Override public boolean isShutdown() { return false; } /** * Call to get the next ready task that is ready to be run. If there are no tasks, or the next * task still has a remaining delay, this will return {@code null}. * <p> * If this is being called in parallel with a {@link #tick(ExecutionHandlerInterface)} call, the * returned task may already be running. You must check the {@code TaskContainer.running} * boolean if this condition is important to you. * * @return next ready task, or {@code null} if there are none */ protected TaskWrapper getNextReadyTask() { TaskWrapper tw = queueManager.getNextTask(); if (tw == null || tw.getScheduleDelay() > 0) { return null; } else { return tw; } } /** * Checks if there are tasks ready to be run on the scheduler. Generally this is called from * the same thread that would call {@link #tick(ExceptionHandler)} (but does not have to be). * If {@link #tick(ExceptionHandler)} is not currently being called, this call indicates if the * next {@link #tick(ExceptionHandler)} will have at least one task to run. If * {@link #tick(ExceptionHandler)} is currently being invoked, this call will do a best attempt * to indicate if there is at least one more task to run (not including the task which may * currently be running). It's a best attempt as it will try not to block the thread invoking * {@link #tick(ExceptionHandler)} to prevent it from accepting additional work. * * @return {@code true} if there are task waiting to run */ public boolean hasTaskReadyToRun() { for (TaskPriority p : TaskPriority.values()) { if (hasTaskReadyToRun(queueManager.getQueueSet(p))) { return true; } } return false; } private static boolean hasTaskReadyToRun(QueueSet queueSet) { if (queueSet.executeQueue.isEmpty()) { TaskWrapper headTask = queueSet.scheduleQueue.peekFirst(); return headTask != null && headTask.getScheduleDelay() <= 0; } else { return true; } } /** * Checks how long till the next task will be ready to execute. If there are no tasks in this * scheduler currently then {@link Long#MAX_VALUE} will be returned. If there is a task ready * to execute this will return a value less than or equal to zero. If the task is past its * desired point of execution the result will be a negative amount of milliseconds past that * point in time. * <p> * Generally this is called from the same thread that would invoke * {@link #tick(ExceptionHandler)} (but does not have to be). Since this does not block or lock * if being invoked in parallel with {@link #tick(ExceptionHandler)}, the results may be no * longer accurate by the time this invocation has returned. * <p> * If a recurring task is currently running this will return a very large number. This will * remain until the task has rescheduled itself. To avoid this just ensure this is never * invoked in parallel with {@link #tick(ExceptionHandler)}. * <p> * This can be useful if you want to know how long you can block on something, ASSUMING you can * detect that something has been added to the scheduler, and interrupt that blocking in order * to handle tasks. * * @return Milliseconds till the next task is ready to run */ public long getDelayTillNextTask() { TaskWrapper tw = queueManager.getNextTask(); if (tw != null) { return tw.getRunTime() - nowInMillis(true); } else { return Long.MAX_VALUE; } } /** * Removes any tasks waiting to be run. Will not interrupt any tasks currently running if * {@link #tick(ExceptionHandler)} is being called. But will avoid additional tasks from being * run on the current {@link #tick(ExceptionHandler)} call. * <p> * If tasks are added concurrently during this invocation they may or may not be removed. * * @return List of runnables which were waiting in the task queue to be executed (and were now removed) */ public List<Runnable> clearTasks() { return queueManager.clearQueue(); } @Override protected QueueManager getQueueManager() { return queueManager; } /** * Wrapper for tasks which only executes once. * * @since 1.0.0 */ protected class NoThreadOneTimeTaskWrapper extends OneTimeTaskWrapper { protected NoThreadOneTimeTaskWrapper(Runnable task, Queue<? extends TaskWrapper> taskQueue, long runTime) { super(task, taskQueue, runTime); } @Override public long getScheduleDelay() { if (getRunTime() > nowInMillis(false)) { return getRunTime() - nowInMillis(true); } else { return 0; } } @Override public void runTask() { if (! invalidated) { // Do not use ExceptionUtils to run task, so that exceptions can be handled in .tick() task.run(); } } } /** * Abstract wrapper for any tasks which run repeatedly. * * @since 4.3.0 */ protected abstract class NoThreadRecurringTaskWrapper extends RecurringTaskWrapper { protected NoThreadRecurringTaskWrapper(Runnable task, QueueSet queueSet, long firstRunTime) { super(task, queueSet, firstRunTime); } @Override public long getScheduleDelay() { if (getRunTime() > nowInMillis(false)) { return getRunTime() - nowInMillis(true); } else { return 0; } } /** * Called when the implementing class should update the variable {@code nextRunTime} to be the * next absolute time in milliseconds the task should run. */ @Override protected abstract void updateNextRunTime(); @Override public void runTask() { if (invalidated) { return; } try { // Do not use ExceptionUtils to run task, so that exceptions can be handled in .tick() task.run(); } finally { if (! invalidated) { updateNextRunTime(); // now that nextRunTime has been set, resort the queue (ask reschedule) reschedule(); // this will set executing to false atomically with the resort } } } } /** * Container for tasks which run with a fixed delay after the previous run. * * @since 4.3.0 */ protected class NoThreadRecurringDelayTaskWrapper extends NoThreadRecurringTaskWrapper { protected final long recurringDelay; protected NoThreadRecurringDelayTaskWrapper(Runnable task, QueueSet queueSet, long firstRunTime, long recurringDelay) { super(task, queueSet, firstRunTime); this.recurringDelay = recurringDelay; } @Override protected void updateNextRunTime() { nextRunTime = nowInMillis(true) + recurringDelay; } } /** * Wrapper for tasks which run at a fixed period (regardless of execution time). * * @since 4.3.0 */ protected class NoThreadRecurringRateTaskWrapper extends NoThreadRecurringTaskWrapper { protected final long period; protected NoThreadRecurringRateTaskWrapper(Runnable task, QueueSet queueSet, long firstRunTime, long period) { super(task, queueSet, firstRunTime); this.period = period; } @Override protected void updateNextRunTime() { nextRunTime += period; } } }