168 lines
4.4 KiB
GDScript3
168 lines
4.4 KiB
GDScript3
|
|
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()
|