2025-01-11 00:25:52 +01:00

168 lines
4.4 KiB
GDScript

class ThreadManager extends Object:
var _thread_registry_lock : ReadWriteLock
var _thread_registry_dict : Dictionary
var _thread_registry : Tuple
var _mutex : RLock = RLock.new()
var _cond : ConditionalVariable = ConditionalVariable.new()
var _cleanup_thread : Thread = Thread.new()
var _shutdown : bool = false
var _finished_threads : Array[StartableBase]
var THREAD_REGISTRY : Tuple:
get: return self._thread_registry
func _init():
self._thread_registry_lock = ReadWriteLock.new()
self._thread_registry_dict = {}
self._thread_registry = Tuple.new([
self._thread_registry_lock,
self._thread_registry_dict
])
self._cleanup_thread.start(self._cleanup)
func stop_and_join():
self._mutex.lock()
self._shutdown = true
self._mutex.unlock()
self._cond.notify_all()
self._cleanup_thread.wait_to_finish()
if not self._finished_threads.is_empty():
assert(false and "should not happen!".length())
## This method adds the thread object to the cleanup thread
## allowing it to be joined and disposed properly.
##
## This is mainly done due to Godot's [method Thread.wait_to_finish] shinanigans.
func finish(thread : StartableBase):
var __ = LockGuard.new(self._mutex)
self._finished_threads.push_back(thread)
self._cond.notify_all()
func _cleanup():
while true:
var thread : StartableBase = null
if true:
var __ = LockGuard.new(self._mutex)
self._cond.wait_pred(self._mutex, func():
return self._shutdown or !self._finished_threads.is_empty())
if (self._shutdown):
break
thread = self._finished_threads.back()
self._finished_threads.pop_back()
thread.wait_to_finish()
## Links the [Thread] instance to a [Callable] before starting it.
class StartableThread extends StartableBase:
var _thread : Thread
var _target : Callable
var _priority : int
var _started : bool
var _joined : AtomicFlag
var thread : Thread:
get: return self._thread
func _init(target : Callable, priority = Thread.PRIORITY_NORMAL):
self._thread = Thread.new()
self._target = target
self._priority = priority
self._started = false
self._joined = AtomicFlag.new()
func _register_thread():
var id = OS.get_thread_caller_id()
var l : ReadWriteLock = GDRx.THREAD_MANAGER.THREAD_REGISTRY.at(0)
l.w_lock()
GDRx.THREAD_MANAGER.THREAD_REGISTRY.at(1)[id] = self._thread
l.w_unlock()
func _deregister_thread():
var id = OS.get_thread_caller_id()
var l : ReadWriteLock = GDRx.THREAD_MANAGER.THREAD_REGISTRY.at(0)
l.w_lock()
GDRx.THREAD_MANAGER.THREAD_REGISTRY.at(1).erase(id)
l.w_unlock()
func start():
if self._started:
GDRx.raise_message("Thread already started!")
return
var run = func():
Thread.set_thread_safety_checks_enabled(false)
self._register_thread()
self._target.call()
self._deregister_thread()
self._started = true
self._thread.start(run, self._priority)
func wait_to_finish():
if self._is_caller():
GDRx.THREAD_MANAGER.finish(self)
return
if self._joined.test_and_set():
return
self._thread.wait_to_finish()
self._thread = null
self._target = GDRx.basic.noop
func _is_caller() -> bool:
return str(OS.get_thread_caller_id()) == self._thread.get_id()
func default_thread_factory(target : Callable) -> StartableThread:
return StartableThread.new(target)
## Dummy class to represent the main [Thread] instance
class MainThreadDummy_ extends Thread:
@warning_ignore("native_method_override")
func start(_callable : Callable, _priority : Priority = PRIORITY_NORMAL) -> Error:
GDRx.raise_message("Do not launch the Main Thread Dummy!")
return FAILED
@warning_ignore("native_method_override")
func wait_to_finish() -> Variant:
GDRx.raise_message("Do not join the Main Thread Dummy!")
return null
func _to_string():
return "MAIN_THREAD::" + str(GDRx.MAIN_THREAD_ID)
@warning_ignore("native_method_override")
func get_id() -> String:
return str(GDRx.MAIN_THREAD_ID)
@warning_ignore("native_method_override")
func is_started() -> bool:
return true
@warning_ignore("native_method_override")
func is_alive() -> bool:
return true
## A naive Atomic Flag
class AtomicFlag:
var _flag : bool
var _mutex : Mutex
func _init():
self._flag = false
self._mutex = Mutex.new()
func test_and_set() -> bool:
var res : bool
self._mutex.lock()
res = self._flag
self._flag = true
self._mutex.unlock()
return res
func clear():
self._mutex.lock()
self._flag = false
self._mutex.unlock()