GD-TaskManager/task_manager.gd

126 lines
4.6 KiB
GDScript3
Raw Normal View History

2024-12-21 13:41:11 -05:00
## Task Manager[br]
##
## A simple [Task] scheduler. This can take a list of functions and process
## them, either in the background or asynchronously.
## Multiple instances of these can be set at different scopes. [br]
##
## Note that due to the nature of processing tasks, if we are to destroy
## referenced objects within task closures, we should await [method TaskManager await_shutdown]
## before unloading said references (nodes, internal state objects, etc)
## otherwise a task may continue to process and use-after-free.
class_name TaskManager extends RefCounted
## Called when a task starts
signal task_started(task:Task)
## Called when a task finishes.
signal task_complete(task:Task)
## Nice name for distinguishing task processors.
var name:String = "Untitled Task Scheduler"
## Array of tasks that are either processing or paused.
## Halted tasks may get GC'd out at any point.
var tasks:Array[Task] = []
## Internal flag. If true, all tasks are to stop when possible.
## Use [method TaskManager.halt_all] if you want this behavior.
var _halt := false
## Initialize. Give a nice name.
func _init(_name:String)->void:
name = _name
## Execute a list of commands. Returns the wrapped up [Task]. Try not to mutate
## the task or you will be killed by a clown under your bed in seven days.[br]
## [param task_name] Identifier for task.[br]
## [param commands] List of prepared functions.[br]
## [param exit_cond] Exit predicate, which can interrupt a process early.[br]
## [param block] If true, will await until the task is complete.[br]
## Otherwise, it'll just start running the task without waiting to return. [br]
## Returns the generated task instance should the origin want to observe it. [br]
## [codeblock]
## var task_manager := TaskManager.new("EXAMPLE")
## # Better if you have coroutines that take time to call. ie awaiting a timeout.
## var commands:Array[Callable] = [print("1")]
## var active_task := task_manager.execute_commands("prints", commands, false)
## print(task_manager.dump_text)
## await active_task.on_finished
## print("Done")
## [/codeblock]
func execute_commands(
task_name:String,
commands:Array[Callable],
exit_cond:Callable = Callable(),
block:bool = true
)->Task:
var new_task := Task.new(task_name, commands, [p_halt_check.bind(exit_cond)])
tasks.append(new_task)
new_task.on_finished.connect(remove_task, CONNECT_ONE_SHOT)
new_task.execute()
if block:
await new_task.finished
return new_task
## Find a given task in this task reference.
## [param task] Target task.
func remove_task(task:Task)->void:
var task_index := tasks.find(task)
if task_index == -1:
return
tasks.remove_at(task_index)
## Flag all tasks to be stopped as soon as possible.
func halt_all()->void:
_halt = true
## Pause or unpause all tasks with [param to_pause].
func set_pause_on_all(to_pause:bool)->void:
Task.set_pause_many(tasks, to_pause)
## If we want this task manager to tick itself, throw it onto a separate channel.
## This lets us be lazy and probably regret it later.[br]
## [param tick_rate] is how many seconds to wait until ticking again.
## We do GC-ing and such for finished tasks, which can be expensive every frame.
func self_process(tick_rate:float = 0.5)->void:
while not _halt:
tick_status_check()
await Utils.COMMANDS.wait_seconds(tick_rate)
## Effectively garbage-collect finished tasks.
func tick_status_check()->void:
#var _is_ongoing := func _is_ongoing(task:Task)->bool: return not task.finished
var _is_finished := func _is_finished(task:Task)->bool: return task.finished
for task:Task in tasks.map(_is_finished):
remove_task(task)
## Sends a halt signal to all processes and awaits until all tasks are safely stopped. [br]
## Await this call to safely ensure you won't have stray processes ticking
## and possibly calling use-after-free references.
func await_shutdown()->void:
halt_all()
# delay until tasks finish and remove themselves.
while not tasks.is_empty():
await Engine.get_main_loop().root.get_tree().process_frame
## Get a string of this scheduler and all the tasks currently in process.
func dump_text()->String:
var scheduler_text := "[%s]"
var reduce := func(accum:String, current:String)->String:
return accum + current + "\n"
var active_tasks := Task.dump_commands(tasks)
var task_megastring:String = active_tasks.reduce(reduce, "")
return scheduler_text + "\n" + task_megastring
## Recursive predicate for halt checking.[br]
## [param p_halt] if true, the task should stop.[br]
## If no [param p_halt] is set, we only will halt if the scheduler is told to halt.
func p_halt_check(p_halt:=Callable())->bool:
if not p_halt.is_valid(): return _halt
return _halt or p_halt.call()