| | 1 | | // Copyright (c) 2020-2024 dotBunny Inc. |
| | 2 | | // dotBunny licenses this file to you under the BSL-1.0 license. |
| | 3 | | // See the LICENSE file in the project root for more information. |
| | 4 | |
|
| | 5 | | using System; |
| | 6 | | using System.Collections.Generic; |
| | 7 | | using System.Threading; |
| | 8 | | using System.Threading.Tasks; |
| | 9 | | using GDX.Collections; |
| | 10 | |
|
| | 11 | | namespace GDX.Threading |
| | 12 | | { |
| | 13 | | /// <summary> |
| | 14 | | /// A simple control mechanism for distributed <see cref="TaskBase" /> work across the |
| | 15 | | /// thread pool. Tasks should be short-lived and can queue up additional work. |
| | 16 | | /// </summary> |
| | 17 | | public static class TaskDirector |
| | 18 | | { |
| | 19 | | /// <summary> |
| | 20 | | /// An event invoked when a <see cref="TaskBase" /> throws an exception. |
| | 21 | | /// </summary> |
| | 22 | | public static Action<Exception> exceptionOccured; |
| | 23 | |
|
| | 24 | | /// <summary> |
| | 25 | | /// An event invoked during <see cref="Tick" /> when user input should be blocked. |
| | 26 | | /// </summary> |
| | 27 | | public static Action<bool> inputBlocked; |
| | 28 | |
|
| | 29 | | /// <summary> |
| | 30 | | /// An event invoked during <see cref="Tick" /> with new log content. |
| | 31 | | /// </summary> |
| | 32 | | public static Action<string[]> logAdded; |
| | 33 | |
|
| | 34 | | /// <summary> |
| | 35 | | /// A running tally of bits that are blocked by the currently executing tasks. |
| | 36 | | /// </summary> |
| 2 | 37 | | static readonly int[] k_BlockedBits = new int[16]; |
| | 38 | |
|
| | 39 | | /// <summary> |
| | 40 | | /// A collection of task names which are currently blocked from beginning to executed based |
| | 41 | | /// on the currently executing tasks. |
| | 42 | | /// </summary> |
| 2 | 43 | | static readonly List<string> k_BlockedNames = new List<string>(); |
| | 44 | |
|
| | 45 | | /// <summary> |
| | 46 | | /// An accumulating collection of log content which will be passed to <see cref="logAdded" /> |
| | 47 | | /// subscribed methods during <see cref="Tick" />. |
| | 48 | | /// </summary> |
| 2 | 49 | | static readonly Queue<string> k_Log = new Queue<string>(10); |
| | 50 | |
|
| | 51 | | /// <summary> |
| | 52 | | /// A locking mechanism used for log entries ensuring thread safety. |
| | 53 | | /// </summary> |
| 2 | 54 | | static readonly object k_LogLock = new object(); |
| | 55 | |
|
| | 56 | | /// <summary> |
| | 57 | | /// A locking mechanism used for changes to task lists ensuring thread safety. |
| | 58 | | /// </summary> |
| 2 | 59 | | static readonly object k_StatusChangeLock = new object(); |
| | 60 | |
|
| | 61 | | /// <summary> |
| | 62 | | /// A list of tasks currently being executed by the thread pool. |
| | 63 | | /// </summary> |
| 2 | 64 | | static readonly List<TaskBase> k_TasksBusy = new List<TaskBase>(); |
| | 65 | |
|
| | 66 | | /// <summary> |
| | 67 | | /// A working list of tasks that recently finished, used in <see cref="Tick" /> to ensure |
| | 68 | | /// callbacks occur on the main thread. |
| | 69 | | /// </summary> |
| 2 | 70 | | static readonly List<TaskBase> k_TasksFinished = new List<TaskBase>(); |
| | 71 | |
|
| | 72 | | /// <summary> |
| | 73 | | /// A list of tasks that were moved from waiting state to a working/busy state during |
| | 74 | | /// <see cref="Tick" />. |
| | 75 | | /// </summary> |
| 2 | 76 | | static readonly List<TaskBase> k_TasksProcessed = new List<TaskBase>(); |
| | 77 | |
|
| | 78 | | /// <summary> |
| | 79 | | /// A list of tasks currently waiting to start work. |
| | 80 | | /// </summary> |
| 2 | 81 | | static readonly List<TaskBase> k_TasksQueue = new List<TaskBase>(); |
| | 82 | |
|
| | 83 | | /// <summary> |
| | 84 | | /// The number of tasks that are busy executing which block all other tasks from executing. |
| | 85 | | /// </summary> |
| | 86 | | /// <remarks> |
| | 87 | | /// This number can be higher then one, when tasks are forcibly started and then added to the |
| | 88 | | /// <see cref="TaskDirector" />. |
| | 89 | | /// </remarks> |
| | 90 | | static int s_BlockAllTasksCount; |
| | 91 | |
|
| | 92 | | /// <summary> |
| | 93 | | /// Is user input blocked? |
| | 94 | | /// </summary> |
| | 95 | | static bool s_BlockInput; |
| | 96 | |
|
| | 97 | | /// <summary> |
| | 98 | | /// The number of tasks that are busy executing which block user input. |
| | 99 | | /// </summary> |
| | 100 | | static int s_BlockInputCount; |
| | 101 | |
|
| | 102 | | /// <summary> |
| | 103 | | /// A cached count of <see cref="k_TasksBusy" />. |
| | 104 | | /// </summary> |
| | 105 | | static int s_TasksBusyCount; |
| | 106 | |
|
| | 107 | | /// <summary> |
| | 108 | | /// A cached count of <see cref="k_TasksQueue" />. |
| | 109 | | /// </summary> |
| | 110 | | static int s_TasksQueueCount; |
| | 111 | |
|
| | 112 | | /// <summary> |
| | 113 | | /// The number of tasks currently in process or awaiting execution by the thread pool. |
| | 114 | | /// </summary> |
| | 115 | | /// <returns>The number of tasks sitting in <see cref="k_TasksBusy" />.</returns> |
| | 116 | | public static int GetBusyCount() |
| 101 | 117 | | { |
| 101 | 118 | | return s_TasksBusyCount; |
| 101 | 119 | | } |
| | 120 | |
|
| | 121 | | /// <summary> |
| | 122 | | /// The number of tasks waiting in the queue. |
| | 123 | | /// </summary> |
| | 124 | | /// <returns>The number of tasks sitting in <see cref="k_TasksQueue" />.</returns> |
| | 125 | | public static int GetQueueCount() |
| 11 | 126 | | { |
| 11 | 127 | | return s_TasksQueueCount; |
| 11 | 128 | | } |
| | 129 | |
|
| | 130 | | /// <summary> |
| | 131 | | /// Get the status message for the <see cref="TaskDirector" />. |
| | 132 | | /// </summary> |
| | 133 | | /// <returns>A pre-formatted status message.</returns> |
| | 134 | | public static string GetStatus() |
| 3 | 135 | | { |
| 3 | 136 | | if (s_TasksBusyCount > 0) |
| 1 | 137 | | { |
| 1 | 138 | | return $"{s_TasksBusyCount.ToString()} Busy / {s_TasksQueueCount.ToString()} Queued"; |
| | 139 | | } |
| | 140 | |
|
| 2 | 141 | | return s_TasksQueueCount > 0 ? $"{s_TasksQueueCount.ToString()} Queued" : null; |
| 3 | 142 | | } |
| | 143 | |
|
| | 144 | | /// <summary> |
| | 145 | | /// Does the <see cref="TaskDirector" /> have any known busy or queued tasks? |
| | 146 | | /// </summary> |
| | 147 | | /// <remarks> |
| | 148 | | /// It's not performant to poll this. |
| | 149 | | /// </remarks> |
| | 150 | | /// <returns>A true/false value indicating tasks.</returns> |
| | 151 | | public static bool HasTasks() |
| 280 | 152 | | { |
| 280 | 153 | | return s_TasksBusyCount > 0 || s_TasksQueueCount > 0; |
| 280 | 154 | | } |
| | 155 | |
|
| | 156 | | /// <summary> |
| | 157 | | /// Is the <see cref="TaskDirector" /> blocking tasks with a specific bit? |
| | 158 | | /// </summary> |
| | 159 | | /// <remarks> |
| | 160 | | /// It isn't ideal to constantly poll this method, ideally this could be used to block things outside of |
| | 161 | | /// the <see cref="TaskDirector" />'s control based on tasks running. |
| | 162 | | /// </remarks> |
| | 163 | | /// <returns>A true/false value indicating if a <see cref="BitArray16" /> index is being blocked.</returns> |
| | 164 | | public static bool IsBlockingBit(int index) |
| 1 | 165 | | { |
| 1 | 166 | | return k_BlockedBits[index] > 0; |
| 1 | 167 | | } |
| | 168 | |
|
| | 169 | | /// <summary> |
| | 170 | | /// Adds a thread-safe log entry to a queue which will be dispatched to <see cref="logAdded" /> on |
| | 171 | | /// the <see cref="Tick" /> invoking thread. |
| | 172 | | /// </summary> |
| | 173 | | /// <param name="message">The log content.</param> |
| | 174 | | public static void Log(string message) |
| 13 | 175 | | { |
| 13 | 176 | | lock (k_LogLock) |
| 13 | 177 | | { |
| 13 | 178 | | k_Log.Enqueue(message); |
| 13 | 179 | | } |
| 13 | 180 | | } |
| | 181 | |
|
| | 182 | | /// <summary> |
| | 183 | | /// Add a task to the queue, to be later started when possible. |
| | 184 | | /// </summary> |
| | 185 | | /// <remarks> |
| | 186 | | /// If the <paramref name="task" /> is already executing it will be added to the known busy list. |
| | 187 | | /// </remarks> |
| | 188 | | /// <param name="task">An established task.</param> |
| | 189 | | public static void QueueTask(TaskBase task) |
| 22 | 190 | | { |
| 22 | 191 | | if (task.IsExecuting()) |
| 1 | 192 | | { |
| | 193 | | // Already running tasks self subscribe |
| 1 | 194 | | return; |
| | 195 | | } |
| | 196 | |
|
| 21 | 197 | | lock (k_StatusChangeLock) |
| 21 | 198 | | { |
| 21 | 199 | | if (k_TasksQueue.Contains(task)) |
| 2 | 200 | | { |
| 2 | 201 | | return; |
| | 202 | | } |
| | 203 | |
|
| 19 | 204 | | k_TasksQueue.Add(task); |
| 19 | 205 | | s_TasksQueueCount++; |
| 19 | 206 | | } |
| 22 | 207 | | } |
| | 208 | |
|
| | 209 | | /// <summary> |
| | 210 | | /// Update the <see cref="TaskDirector" />, evaluating known tasks for work eligibility and execution. |
| | 211 | | /// </summary> |
| | 212 | | /// <remarks> |
| | 213 | | /// This should occur on the main thread. If the <see cref="TaskDirector" /> is used during play mode, |
| | 214 | | /// something needs to call this every global tick. While in edit mode the EditorTaskDirector triggers this |
| | 215 | | /// method. |
| | 216 | | /// </remarks> |
| | 217 | | public static void Tick() |
| 349 | 218 | | { |
| | 219 | | // We are blocked by a running task from adding anything else. |
| 349 | 220 | | lock (k_StatusChangeLock) |
| 349 | 221 | | { |
| 349 | 222 | | int finishedWorkersCount = k_TasksFinished.Count; |
| 349 | 223 | | if (finishedWorkersCount > 0) |
| 20 | 224 | | { |
| 80 | 225 | | for (int i = 0; i < finishedWorkersCount; i++) |
| 20 | 226 | | { |
| 20 | 227 | | TaskBase taskBase = k_TasksFinished[i]; |
| 20 | 228 | | taskBase.completedMainThread?.Invoke(taskBase); |
| 20 | 229 | | } |
| | 230 | |
|
| 20 | 231 | | k_TasksFinished.Clear(); |
| 20 | 232 | | } |
| | 233 | |
|
| 349 | 234 | | if (s_BlockAllTasksCount == 0) |
| 322 | 235 | | { |
| | 236 | | // Spin up workers needed to process |
| 322 | 237 | | int count = k_TasksQueue.Count; |
| | 238 | |
|
| 322 | 239 | | if (count > 0) |
| 46 | 240 | | { |
| 206 | 241 | | for (int i = 0; i < count; i++) |
| 57 | 242 | | { |
| 57 | 243 | | TaskBase task = k_TasksQueue[i]; |
| | 244 | |
|
| | 245 | | // Check if task has a blocked name |
| 57 | 246 | | if (k_BlockedNames.Contains(task.GetName())) |
| 30 | 247 | | { |
| 30 | 248 | | continue; |
| | 249 | | } |
| | 250 | |
|
| 27 | 251 | | BitArray16 bits = task.GetBits(); |
| 27 | 252 | | if (IsBlockedByBits(ref bits)) |
| 8 | 253 | | { |
| 8 | 254 | | continue; |
| | 255 | | } |
| | 256 | |
|
| 19 | 257 | | AddBusyTask(task); |
| 76 | 258 | | ThreadPool.QueueUserWorkItem(delegate { task.Run(); }); |
| 19 | 259 | | k_TasksProcessed.Add(task); |
| 19 | 260 | | } |
| | 261 | |
|
| 46 | 262 | | int processedCount = k_TasksProcessed.Count; |
| 130 | 263 | | for (int i = 0; i < processedCount; i++) |
| 19 | 264 | | { |
| 19 | 265 | | k_TasksQueue.Remove(k_TasksProcessed[i]); |
| 19 | 266 | | } |
| | 267 | |
|
| 46 | 268 | | s_TasksQueueCount = k_TasksQueue.Count; |
| 46 | 269 | | k_TasksProcessed.Clear(); |
| 46 | 270 | | } |
| 322 | 271 | | } |
| 349 | 272 | | } |
| | 273 | |
|
| | 274 | | // Dispatch logging |
| 349 | 275 | | lock (k_LogLock) |
| 349 | 276 | | { |
| 349 | 277 | | if (k_Log.Count > 0) |
| 3 | 278 | | { |
| 3 | 279 | | logAdded?.Invoke(k_Log.ToArray()); |
| 3 | 280 | | k_Log.Clear(); |
| 3 | 281 | | } |
| 349 | 282 | | } |
| | 283 | |
|
| | 284 | | // Invoke notification to anything subscribed to block input |
| 349 | 285 | | if (s_BlockInputCount > 0 && !s_BlockInput) |
| 3 | 286 | | { |
| 3 | 287 | | inputBlocked?.Invoke(true); |
| 3 | 288 | | s_BlockInput = true; |
| 3 | 289 | | } |
| 346 | 290 | | else if (s_BlockInputCount <= 0 && s_BlockInput) |
| 3 | 291 | | { |
| 3 | 292 | | inputBlocked?.Invoke(false); |
| 3 | 293 | | s_BlockInput = false; |
| 3 | 294 | | } |
| 349 | 295 | | } |
| | 296 | |
|
| | 297 | | /// <summary> |
| | 298 | | /// Evaluate the provided task and update its state inside of the <see cref="TaskDirector" />. |
| | 299 | | /// </summary> |
| | 300 | | /// <remarks> |
| | 301 | | /// This will add a task to the <see cref="TaskDirector" /> if it does not already know about it, regardless |
| | 302 | | /// of the current blocking mode status. Do not use this method to add non executing tasks, they will not |
| | 303 | | /// be added to the <see cref="TaskDirector" /> in this way. |
| | 304 | | /// </remarks> |
| | 305 | | /// <param name="task">An established task.</param> |
| | 306 | | public static void UpdateTask(TaskBase task) |
| 40 | 307 | | { |
| 40 | 308 | | if (task.IsDone()) |
| 20 | 309 | | { |
| 20 | 310 | | RemoveBusyTask(task); |
| 20 | 311 | | } |
| 20 | 312 | | else if (task.IsExecuting()) |
| 20 | 313 | | { |
| 20 | 314 | | AddBusyTask(task); |
| 20 | 315 | | } |
| 40 | 316 | | } |
| | 317 | |
|
| | 318 | | /// <summary> |
| | 319 | | /// Wait on the completion of all known tasks, blocking the invoking thread. |
| | 320 | | /// </summary> |
| | 321 | | /// <remarks> |
| | 322 | | /// Useful to force the main thread to wait for completion of tasks. |
| | 323 | | /// </remarks> |
| | 324 | | public static void Wait() |
| 23 | 325 | | { |
| 220 | 326 | | while (HasTasks()) |
| 197 | 327 | | { |
| 197 | 328 | | Thread.Sleep(1); |
| 197 | 329 | | Tick(); |
| 197 | 330 | | } |
| | 331 | |
|
| 23 | 332 | | Tick(); |
| 23 | 333 | | } |
| | 334 | |
|
| | 335 | | /// <summary> |
| | 336 | | /// Asynchronously wait on the completion of all known tasks. |
| | 337 | | /// </summary> |
| | 338 | | public static async Task WaitAsync() |
| 18 | 339 | | { |
| 55 | 340 | | while (HasTasks()) |
| 37 | 341 | | { |
| 111 | 342 | | await Task.Delay(1); |
| 37 | 343 | | Tick(); |
| 37 | 344 | | } |
| | 345 | |
|
| 18 | 346 | | Tick(); |
| 18 | 347 | | } |
| | 348 | |
|
| | 349 | |
|
| | 350 | | /// <summary> |
| | 351 | | /// Add a <see cref="TaskBase" /> to the known list of working tasks. |
| | 352 | | /// </summary> |
| | 353 | | /// <remarks> |
| | 354 | | /// This will add the blocking mode settings to the current settings. |
| | 355 | | /// </remarks> |
| | 356 | | /// <param name="task">An established task.</param> |
| | 357 | | static void AddBusyTask(TaskBase task) |
| 39 | 358 | | { |
| 39 | 359 | | lock (k_StatusChangeLock) |
| 39 | 360 | | { |
| 39 | 361 | | if (!k_TasksBusy.Contains(task)) |
| 20 | 362 | | { |
| 20 | 363 | | if (task.IsBlockingAllTasks()) |
| 5 | 364 | | { |
| 5 | 365 | | s_BlockAllTasksCount++; |
| 5 | 366 | | } |
| | 367 | |
|
| | 368 | | // Add to the count of tasks that block input so we can update based off it |
| 20 | 369 | | if (task.IsBlockingUserInterface()) |
| 3 | 370 | | { |
| 3 | 371 | | s_BlockInputCount++; |
| 3 | 372 | | } |
| | 373 | |
|
| 20 | 374 | | if (task.IsBlockingSameName()) |
| 7 | 375 | | { |
| 7 | 376 | | k_BlockedNames.Add(task.GetName()); |
| 7 | 377 | | } |
| | 378 | |
|
| 20 | 379 | | if (task.IsBlockingBits()) |
| 2 | 380 | | { |
| 2 | 381 | | BitArray16 blockedBits = task.GetBlockedBits(); |
| 68 | 382 | | for (int i = 0; i < 16; i++) |
| 32 | 383 | | { |
| 32 | 384 | | if (blockedBits[(byte)i]) |
| 2 | 385 | | { |
| 2 | 386 | | k_BlockedBits[i]++; |
| 2 | 387 | | } |
| 32 | 388 | | } |
| 2 | 389 | | } |
| | 390 | |
|
| 20 | 391 | | k_TasksBusy.Add(task); |
| 20 | 392 | | s_TasksBusyCount++; |
| 20 | 393 | | } |
| 39 | 394 | | } |
| 39 | 395 | | } |
| | 396 | |
|
| | 397 | | /// <summary> |
| | 398 | | /// Is the provided bit array blocked by the current blocking settings. |
| | 399 | | /// </summary> |
| | 400 | | /// <param name="bits">A <see cref="TaskBase" />'s bits.</param> |
| | 401 | | /// <returns>true/false if the task should be blocked from executing.</returns> |
| | 402 | | static bool IsBlockedByBits(ref BitArray16 bits) |
| 27 | 403 | | { |
| 678 | 404 | | for (int i = 0; i < 16; i++) |
| 320 | 405 | | { |
| 320 | 406 | | if (bits[(byte)i] && k_BlockedBits[i] > 0) |
| 8 | 407 | | { |
| 8 | 408 | | return true; |
| | 409 | | } |
| 312 | 410 | | } |
| | 411 | |
|
| 19 | 412 | | return false; |
| 27 | 413 | | } |
| | 414 | |
|
| | 415 | | /// <summary> |
| | 416 | | /// Remove a <see cref="TaskBase" /> from the known list of working tasks. |
| | 417 | | /// </summary> |
| | 418 | | /// <remarks> |
| | 419 | | /// This will remove the blocking mode settings to the current settings. |
| | 420 | | /// </remarks> |
| | 421 | | /// <param name="task">An established task.</param> |
| | 422 | | static void RemoveBusyTask(TaskBase task) |
| 20 | 423 | | { |
| 20 | 424 | | lock (k_StatusChangeLock) |
| 20 | 425 | | { |
| 20 | 426 | | if (k_TasksBusy.Contains(task)) |
| 20 | 427 | | { |
| 20 | 428 | | k_TasksBusy.Remove(task); |
| 20 | 429 | | s_TasksBusyCount--; |
| | 430 | |
|
| | 431 | | // Add to list of tasks so that the next tick the main thread will call their completion callbacks. |
| 20 | 432 | | k_TasksFinished.Add(task); |
| | 433 | |
|
| 20 | 434 | | if (task.IsBlockingAllTasks()) |
| 5 | 435 | | { |
| 5 | 436 | | s_BlockAllTasksCount--; |
| 5 | 437 | | } |
| | 438 | |
|
| 20 | 439 | | if (task.IsBlockingUserInterface()) |
| 3 | 440 | | { |
| 3 | 441 | | s_BlockInputCount--; |
| 3 | 442 | | } |
| | 443 | |
|
| 20 | 444 | | if (task.IsBlockingSameName()) |
| 7 | 445 | | { |
| 7 | 446 | | k_BlockedNames.Remove(task.GetName()); |
| 7 | 447 | | } |
| | 448 | |
|
| 20 | 449 | | if (task.IsBlockingBits()) |
| 2 | 450 | | { |
| 2 | 451 | | BitArray16 blockedBits = task.GetBlockedBits(); |
| 68 | 452 | | for (int i = 0; i < 16; i++) |
| 32 | 453 | | { |
| 32 | 454 | | if (blockedBits[(byte)i]) |
| 2 | 455 | | { |
| 2 | 456 | | k_BlockedBits[i]--; |
| 2 | 457 | | } |
| 32 | 458 | | } |
| 2 | 459 | | } |
| 20 | 460 | | } |
| | 461 | |
|
| 20 | 462 | | if (task.IsFaulted()) |
| 2 | 463 | | { |
| 2 | 464 | | exceptionOccured?.Invoke(task.GetException()); |
| 2 | 465 | | } |
| 20 | 466 | | } |
| 20 | 467 | | } |
| | 468 | | } |
| | 469 | | } |