1 /* 2 * Entreri, an entity-component framework in Java 3 * 4 * Copyright (c) 2013, Michael Ludwig 5 * All rights reserved. 6 * 7 * Redistribution and use in source and binary forms, with or without modification, 8 * are permitted provided that the following conditions are met: 9 * 10 * Redistributions of source code must retain the above copyright notice, 11 * this list of conditions and the following disclaimer. 12 * Redistributions in binary form must reproduce the above copyright notice, 13 * this list of conditions and the following disclaimer in the 14 * documentation and/or other materials provided with the distribution. 15 * 16 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 20 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 23 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 */ 27 package com.lhkbob.entreri.task; 28 29 import com.lhkbob.entreri.Component; 30 import com.lhkbob.entreri.EntitySystem; 31 32 import java.util.concurrent.*; 33 import java.util.concurrent.locks.ReentrantLock; 34 import java.util.concurrent.locks.ReentrantReadWriteLock; 35 36 /** 37 * <p/> 38 * Scheduler coordinates the multi-threaded execution of jobs that process an 39 * EntitySystem. It is the factory that creates jobs and contains convenience methods to 40 * schedule the execution of jobs. 41 * <p/> 42 * As an example, here's how you can set up a 'rendering' job, assuming that all tasks 43 * necessary to perform rendering are in an array called <var>renderTasks</var>: 44 * <p/> 45 * <pre> 46 * Job renderJob = system.getScheduler().createJob("rendering", renderTasks); 47 * ExecutorService service = system.getScheduler().runEvery(1.0 / 60.0, renderJob); 48 * 49 * // ... perform game logic, and wait for exit request 50 * service.shutdown(); 51 * </pre> 52 * 53 * @author Michael Ludwig 54 */ 55 public class Scheduler { 56 private final ThreadGroup schedulerGroup; 57 58 // write lock is for tasks that add/remove entities, 59 // read lock is for all other tasks 60 private final ReentrantReadWriteLock exclusiveLock; 61 62 // locks per component data type, this map is filled 63 // dynamically the first time each type is requested 64 private final ConcurrentHashMap<Class<? extends Component>, ReentrantLock> typeLocks; 65 66 private final EntitySystem system; 67 68 /** 69 * Create a new Scheduler for the given EntitySystem. It is recommended to use the 70 * scheduler provided by the system. If multiple schedulers exist for the same entity 71 * system, they cannot guarantee thread safety between each other, only within their 72 * own jobs. 73 * 74 * @param system The EntitySystem accessed by jobs created by this scheduler 75 * 76 * @throws NullPointerException if system is null 77 * @see EntitySystem#getScheduler() 78 */ 79 public Scheduler(EntitySystem system) { 80 if (system == null) { 81 throw new NullPointerException("EntitySystem cannot be null"); 82 } 83 this.system = system; 84 85 schedulerGroup = new ThreadGroup("job-scheduler"); 86 exclusiveLock = new ReentrantReadWriteLock(); 87 typeLocks = new ConcurrentHashMap<>(); 88 } 89 90 /** 91 * @return The EntitySystem accessed by this scheduler 92 */ 93 public EntitySystem getEntitySystem() { 94 return system; 95 } 96 97 /** 98 * @return The read-write lock used to coordinate entity data access 99 */ 100 ReentrantReadWriteLock getEntitySystemLock() { 101 return exclusiveLock; 102 } 103 104 /** 105 * @param id The component type to lock 106 * 107 * @return The lock used to coordinate access to the particular componen type 108 */ 109 ReentrantLock getTypeLock(Class<? extends Component> id) { 110 ReentrantLock lock = typeLocks.get(id); 111 if (lock == null) { 112 // this will either return the newly constructed lock, or 113 // the lock inserted from another thread after we tried to fetch it, 114 // in either case, the lock is valid 115 ReentrantLock newLock = new ReentrantLock(); 116 lock = typeLocks.putIfAbsent(id, newLock); 117 if (lock == null) { 118 // newLock was the assigned one 119 lock = newLock; 120 } 121 } 122 return lock; 123 } 124 125 /** 126 * Create a new job with the given <var>name</var>, that will execute the provided 127 * tasks in order. 128 * 129 * @param name The name of the new job 130 * @param tasks The tasks of the job 131 * 132 * @return The new job 133 * 134 * @throws NullPointerException if name is null, tasks is null or contains null 135 * elements 136 */ 137 public Job createJob(String name, Task... tasks) { 138 return new Job(name, this, tasks); 139 } 140 141 /** 142 * Execute the given job on the current thread. This will not return until after the 143 * job has completed invoking all of its tasks, and any subsequently produced tasks. 144 * <p/> 145 * This is a convenience for invoking {@link Job#run()}, and exists primarily to 146 * parallel the other runX(Job) methods. 147 * 148 * @param job The job to run 149 * 150 * @throws NullPointerException if job is null 151 * @throws IllegalArgumentException if job was not created by this scheduler 152 */ 153 public void runOnCurrentThread(Job job) { 154 if (job == null) { 155 throw new NullPointerException("Job cannot be null"); 156 } 157 if (job.getScheduler() != this) { 158 throw new IllegalArgumentException( 159 "Job was created by a different scheduler"); 160 } 161 162 // the job will handle all locking logic 163 job.run(); 164 } 165 166 /** 167 * <p/> 168 * Execute the given job once on a new thread. This will create a new thread that will 169 * invoke the job once and then terminate once the job returns. This method will 170 * return after the thread starts and will not block the calling thread while the job 171 * is executed. 172 * <p/> 173 * This should be used as a convenience to invoke one-off jobs that should not block a 174 * performance sensitive thread. 175 * 176 * @param job The job to run 177 * 178 * @throws NullPointerException if job is null 179 * @throws IllegalArgumentException if job was not created by this scheduler 180 */ 181 public void runOnSeparateThread(Job job) { 182 if (job == null) { 183 throw new NullPointerException("Job cannot be null"); 184 } 185 if (job.getScheduler() != this) { 186 throw new IllegalArgumentException( 187 "Job was created by a different scheduler"); 188 } 189 190 // spawn a new thread that will terminate when the job completes 191 Thread jobThread = new Thread(schedulerGroup, job, "job-" + job.getName()); 192 jobThread.start(); 193 } 194 195 /** 196 * <p/> 197 * Create an ExecutorService that is configured to execute the given job every 198 * <var>dt</var> seconds. Assuming that the job terminates in under <var>dt</var> 199 * seconds, it will not be invoked until <var>dt</var> seconds after it was first 200 * started. 201 * <p/> 202 * To schedule a rendering job to run at 60 FPS, you could call <code>runEvery(1.0 / 203 * 60.0, renderJob)</code>. 204 * <p/> 205 * The returned ExecutorService should have its {@link ExecutorService#shutdown() 206 * shutdown()} method called when the job no longer needs to be invoked. Scheduling 207 * timing is undefined if new Runnables or Callables are submitted to the returned 208 * service. 209 * 210 * @param dt The amount of time between the start of each job execution 211 * @param job The job to be repeatedly executed 212 * 213 * @return An unconfigurable executor service that performs the scheduling, and owns 214 * the execution thread 215 * 216 * @throws NullPointerException if job is null 217 * @throws IllegalArgumentException if job was not created by this scheduler, or if dt 218 * is negative 219 */ 220 public ExecutorService runEvery(double dt, Job job) { 221 if (job == null) { 222 throw new NullPointerException("Job cannot be null"); 223 } 224 if (job.getScheduler() != this) { 225 throw new IllegalArgumentException( 226 "Job was created by a different scheduler"); 227 } 228 if (dt < 0) { 229 throw new IllegalArgumentException( 230 "Time between jobs cannot be negative: " + dt); 231 } 232 233 final String name = String.format("job-%s-every-%.2fs", job.getName(), dt); 234 ScheduledExecutorService service = Executors 235 .newSingleThreadScheduledExecutor(new ThreadFactory() { 236 @Override 237 public Thread newThread(Runnable r) { 238 return new Thread(schedulerGroup, r, name); 239 } 240 }); 241 service.scheduleAtFixedRate(job, 0L, (long) (dt * 1e9), TimeUnit.NANOSECONDS); 242 return Executors.unconfigurableExecutorService(service); 243 } 244 245 /** 246 * <p/> 247 * Create an ExecutorService that is configured to execute the given job back to back 248 * as fast as the job executes. 249 * <p/> 250 * This effectively performs the following logic on a separate thread: 251 * <p/> 252 * <pre> 253 * while (true) { 254 * job.run(); 255 * } 256 * </pre> 257 * <p/> 258 * The returned ExecutorService should have its {@link ExecutorService#shutdown() 259 * shutdown()} method called when the job no longer needs to be invoked. Scheduling 260 * timing is undefined if new Runnables or Callables are submitted to the returned 261 * service. 262 * 263 * @param job The job to be repeatedly executed 264 * 265 * @return An unconfigurable executor service that performs the scheduling, and owns 266 * the execution thread 267 * 268 * @throws NullPointerException if job is null 269 * @throws IllegalArgumentException if job was not created by this scheduler 270 */ 271 public ExecutorService runContinuously(Job job) { 272 if (job == null) { 273 throw new NullPointerException("Job cannot be null"); 274 } 275 if (job.getScheduler() != this) { 276 throw new IllegalArgumentException( 277 "Job was created by a different scheduler"); 278 } 279 280 final String name = String.format("job-%s-as-fast-as-possible", job.getName()); 281 ScheduledExecutorService service = Executors 282 .newSingleThreadScheduledExecutor(new ThreadFactory() { 283 @Override 284 public Thread newThread(Runnable r) { 285 return new Thread(schedulerGroup, r, name); 286 } 287 }); 288 289 // ScheduledExecutorService has no way to just specify run-as-fast-as-possible. 290 // However, if a task takes longer than its fixed-rate, that is the resulting, 291 // behavior. There is a strong probability that all jobs will take longer 292 // than a single nanosecond, so this should do the trick. 293 service.scheduleAtFixedRate(job, 0L, 1L, TimeUnit.NANOSECONDS); 294 return Executors.unconfigurableExecutorService(service); 295 } 296 }