View Javadoc

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(&quot;rendering&quot;, 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 }