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 }