| | | 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() |
| | 282 | 152 | | { |
| | 282 | 153 | | return s_TasksBusyCount > 0 || s_TasksQueueCount > 0; |
| | 282 | 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() |
| | 351 | 218 | | { |
| | | 219 | | // We are blocked by a running task from adding anything else. |
| | 351 | 220 | | lock (k_StatusChangeLock) |
| | 351 | 221 | | { |
| | 351 | 222 | | int finishedWorkersCount = k_TasksFinished.Count; |
| | 351 | 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 | | |
| | 351 | 234 | | if (s_BlockAllTasksCount == 0) |
| | 324 | 235 | | { |
| | | 236 | | // Spin up workers needed to process |
| | 324 | 237 | | int count = k_TasksQueue.Count; |
| | | 238 | | |
| | 324 | 239 | | if (count > 0) |
| | 47 | 240 | | { |
| | 212 | 241 | | for (int i = 0; i < count; i++) |
| | 59 | 242 | | { |
| | 59 | 243 | | TaskBase task = k_TasksQueue[i]; |
| | | 244 | | |
| | | 245 | | // Check if task has a blocked name |
| | 59 | 246 | | if (k_BlockedNames.Contains(task.GetName())) |
| | 33 | 247 | | { |
| | 33 | 248 | | continue; |
| | | 249 | | } |
| | | 250 | | |
| | 26 | 251 | | BitArray16 bits = task.GetBits(); |
| | 26 | 252 | | if (IsBlockedByBits(ref bits)) |
| | 7 | 253 | | { |
| | 7 | 254 | | continue; |
| | | 255 | | } |
| | | 256 | | |
| | 19 | 257 | | AddBusyTask(task); |
| | 76 | 258 | | ThreadPool.QueueUserWorkItem(delegate { task.Run(); }); |
| | 19 | 259 | | k_TasksProcessed.Add(task); |
| | 19 | 260 | | } |
| | | 261 | | |
| | 47 | 262 | | int processedCount = k_TasksProcessed.Count; |
| | 132 | 263 | | for (int i = 0; i < processedCount; i++) |
| | 19 | 264 | | { |
| | 19 | 265 | | k_TasksQueue.Remove(k_TasksProcessed[i]); |
| | 19 | 266 | | } |
| | | 267 | | |
| | 47 | 268 | | s_TasksQueueCount = k_TasksQueue.Count; |
| | 47 | 269 | | k_TasksProcessed.Clear(); |
| | 47 | 270 | | } |
| | 324 | 271 | | } |
| | 351 | 272 | | } |
| | | 273 | | |
| | | 274 | | // Dispatch logging |
| | 351 | 275 | | lock (k_LogLock) |
| | 351 | 276 | | { |
| | 351 | 277 | | if (k_Log.Count > 0) |
| | 3 | 278 | | { |
| | 3 | 279 | | logAdded?.Invoke(k_Log.ToArray()); |
| | 3 | 280 | | k_Log.Clear(); |
| | 3 | 281 | | } |
| | 351 | 282 | | } |
| | | 283 | | |
| | | 284 | | // Invoke notification to anything subscribed to block input |
| | 351 | 285 | | if (s_BlockInputCount > 0 && !s_BlockInput) |
| | 3 | 286 | | { |
| | 3 | 287 | | inputBlocked?.Invoke(true); |
| | 3 | 288 | | s_BlockInput = true; |
| | 3 | 289 | | } |
| | 348 | 290 | | else if (s_BlockInputCount <= 0 && s_BlockInput) |
| | 3 | 291 | | { |
| | 3 | 292 | | inputBlocked?.Invoke(false); |
| | 3 | 293 | | s_BlockInput = false; |
| | 3 | 294 | | } |
| | 351 | 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 | | { |
| | 223 | 326 | | while (HasTasks()) |
| | 200 | 327 | | { |
| | 200 | 328 | | Thread.Sleep(1); |
| | 200 | 329 | | Tick(); |
| | 200 | 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 | | { |
| | 54 | 340 | | while (HasTasks()) |
| | 36 | 341 | | { |
| | 108 | 342 | | await Task.Delay(1); |
| | 36 | 343 | | Tick(); |
| | 36 | 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) |
| | 26 | 403 | | { |
| | 674 | 404 | | for (int i = 0; i < 16; i++) |
| | 318 | 405 | | { |
| | 318 | 406 | | if (bits[(byte)i] && k_BlockedBits[i] > 0) |
| | 7 | 407 | | { |
| | 7 | 408 | | return true; |
| | | 409 | | } |
| | 311 | 410 | | } |
| | | 411 | | |
| | 19 | 412 | | return false; |
| | 26 | 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 | | } |