Skip to content

Commit

Permalink
Fixup thread-owned lambda bookkeeping on thread exit
Browse files Browse the repository at this point in the history
  • Loading branch information
RandomShaper committed Nov 9, 2023
1 parent f3e96a8 commit 2715117
Show file tree
Hide file tree
Showing 4 changed files with 107 additions and 18 deletions.
104 changes: 91 additions & 13 deletions modules/gdscript/gdscript.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1387,33 +1387,106 @@ String GDScript::debug_get_script_name(const Ref<Script> &p_script) {
#endif

thread_local GDScript::UpdatableFuncPtr GDScript::func_ptrs_to_update_thread_local;
GDScript::UpdatableFuncPtr *GDScript::func_ptrs_to_update_main_thread = &func_ptrs_to_update_thread_local;

GDScript::UpdatableFuncPtrElement GDScript::_add_func_ptr_to_update(GDScriptFunction **p_func_ptr_ptr) {
UpdatableFuncPtrElement result = {};
GDScript::UpdatableFuncPtrElement *GDScript::_add_func_ptr_to_update(GDScriptFunction **p_func_ptr_ptr) {
MutexLock lock(func_ptrs_to_update_mutex);

List<UpdatableFuncPtrElement>::Element *result = func_ptrs_to_update_elems.push_back(UpdatableFuncPtrElement());

{
MutexLock lock(func_ptrs_to_update_thread_local.mutex);
result.element = func_ptrs_to_update_thread_local.ptrs.push_back(p_func_ptr_ptr);
result.mutex = &func_ptrs_to_update_thread_local.mutex;
MutexLock lock2(func_ptrs_to_update_thread_local.mutex);
result->get().element = func_ptrs_to_update_thread_local.ptrs.push_back(p_func_ptr_ptr);
result->get().mutex = &func_ptrs_to_update_thread_local.mutex;

if (likely(func_ptrs_to_update_thread_local.initialized)) {
return result;
return &result->get();
}

func_ptrs_to_update_thread_local.initialized = true;
}

MutexLock lock(func_ptrs_to_update_mutex);
func_ptrs_to_update.push_back(&func_ptrs_to_update_thread_local);

return result;
return &result->get();
}

void GDScript::_remove_func_ptr_to_update(const UpdatableFuncPtrElement p_func_ptr_element) {
ERR_FAIL_NULL(p_func_ptr_element.element);
ERR_FAIL_NULL(p_func_ptr_element.mutex);
MutexLock lock(*p_func_ptr_element.mutex);
p_func_ptr_element.element->erase();
void GDScript::_remove_func_ptr_to_update(const UpdatableFuncPtrElement *p_func_ptr_element) {
// None of these checks should ever fail, unless there's a bug.
// They can be removed once we are sure they never catch anything.
// Left here now due to extra safety needs late in the release cycle.
ERR_FAIL_NULL(p_func_ptr_element);
MutexLock lock(*p_func_ptr_element->mutex);
ERR_FAIL_NULL(p_func_ptr_element->element);
ERR_FAIL_NULL(p_func_ptr_element->mutex);
p_func_ptr_element->element->erase();
}

void GDScript::_fixup_thread_function_bookkeeping() {
// Transfer the ownership of these update items to the main thread,
// because the current one is dying, leaving theirs orphan, dangling.

HashSet<GDScript *> scripts;

DEV_ASSERT(!Thread::is_main_thread());
MutexLock lock(func_ptrs_to_update_main_thread->mutex);

{
MutexLock lock2(func_ptrs_to_update_thread_local.mutex);

while (!func_ptrs_to_update_thread_local.ptrs.is_empty()) {
// Transfer the thread-to-script records from the dying thread to the main one.

List<GDScriptFunction **>::Element *E = func_ptrs_to_update_thread_local.ptrs.front();
List<GDScriptFunction **>::Element *new_E = func_ptrs_to_update_main_thread->ptrs.push_front(E->get());

GDScript *script = (*E->get())->get_script();
if (!scripts.has(script)) {
scripts.insert(script);

// Replace dying thread by the main thread in the script-to-thread records.

MutexLock lock3(script->func_ptrs_to_update_mutex);
DEV_ASSERT(script->func_ptrs_to_update.find(&func_ptrs_to_update_thread_local));
{
for (List<UpdatableFuncPtrElement>::Element *F = script->func_ptrs_to_update_elems.front(); F; F = F->next()) {
bool is_dying_thread_entry = F->get().mutex == &func_ptrs_to_update_thread_local.mutex;
if (is_dying_thread_entry) {
// This may lead to multiple main-thread entries, but that's not a problem
// and allows to reuse the element, which is needed, since it's tracked by pointer.
F->get().element = new_E;
F->get().mutex = &func_ptrs_to_update_main_thread->mutex;
}
}
}
}

E->erase();
}
}
func_ptrs_to_update_main_thread->initialized = true;

{
// Remove orphan thread-to-script entries from every script.
// FIXME: This involves iterating through every script whenever a thread dies.
// While it's OK that thread creation/destruction are heavy operations,
// additional bookkeeping can be used to outperform this brute-force approach.

GDScriptLanguage *gd_lang = GDScriptLanguage::get_singleton();

MutexLock lock2(gd_lang->mutex);

for (SelfList<GDScript> *s = gd_lang->script_list.first(); s; s = s->next()) {
GDScript *script = s->self();
for (List<UpdatableFuncPtr *>::Element *E = script->func_ptrs_to_update.front(); E; E = E->next()) {
bool is_dying_thread_entry = &E->get()->mutex == &func_ptrs_to_update_thread_local.mutex;
if (is_dying_thread_entry) {
E->erase();
break;
}
}
}
}
}

void GDScript::clear(ClearData *p_clear_data) {
Expand Down Expand Up @@ -1441,6 +1514,7 @@ void GDScript::clear(ClearData *p_clear_data) {
*func_ptr_ptr = nullptr;
}
}
func_ptrs_to_update_elems.clear();
}

RBSet<GDScript *> must_clear_dependencies = get_must_clear_dependencies();
Expand Down Expand Up @@ -2060,6 +2134,10 @@ void GDScriptLanguage::remove_named_global_constant(const StringName &p_name) {
named_globals.erase(p_name);
}

void GDScriptLanguage::thread_exit() {
GDScript::_fixup_thread_function_bookkeeping();
}

void GDScriptLanguage::init() {
//populate global constants
int gcc = CoreConstants::get_global_constant_count();
Expand Down
13 changes: 11 additions & 2 deletions modules/gdscript/gdscript.h
Original file line number Diff line number Diff line change
Expand Up @@ -128,11 +128,16 @@ class GDScript : public Script {
Mutex *mutex = nullptr;
};
static thread_local UpdatableFuncPtr func_ptrs_to_update_thread_local;
static thread_local LocalVector<List<UpdatableFuncPtr *>::Element> func_ptrs_to_update_entries_thread_local;
static UpdatableFuncPtr *func_ptrs_to_update_main_thread;
List<UpdatableFuncPtr *> func_ptrs_to_update;
List<UpdatableFuncPtrElement> func_ptrs_to_update_elems;
Mutex func_ptrs_to_update_mutex;

UpdatableFuncPtrElement _add_func_ptr_to_update(GDScriptFunction **p_func_ptr_ptr);
static void _remove_func_ptr_to_update(const UpdatableFuncPtrElement p_func_ptr_element);
UpdatableFuncPtrElement *_add_func_ptr_to_update(GDScriptFunction **p_func_ptr_ptr);
static void _remove_func_ptr_to_update(const UpdatableFuncPtrElement *p_func_ptr_element);

static void _fixup_thread_function_bookkeeping();

#ifdef TOOLS_ENABLED
// For static data storage during hot-reloading.
Expand Down Expand Up @@ -553,6 +558,10 @@ class GDScriptLanguage : public ScriptLanguage {
virtual void add_named_global_constant(const StringName &p_name, const Variant &p_value) override;
virtual void remove_named_global_constant(const StringName &p_name) override;

/* MULTITHREAD FUNCTIONS */

virtual void thread_exit() override;

/* DEBUGGER FUNCTIONS */

virtual String debug_get_error() const override;
Expand Down
4 changes: 3 additions & 1 deletion modules/gdscript/gdscript_lambda_callable.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -296,5 +296,7 @@ GDScriptLambdaSelfCallable::GDScriptLambdaSelfCallable(Object *p_self, GDScriptF
}

GDScriptLambdaSelfCallable::~GDScriptLambdaSelfCallable() {
GDScript::_remove_func_ptr_to_update(updatable_func_ptr_element);
if (updatable_func_ptr_element) {
GDScript::_remove_func_ptr_to_update(updatable_func_ptr_element);
}
}
4 changes: 2 additions & 2 deletions modules/gdscript/gdscript_lambda_callable.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ class GDScriptLambdaCallable : public CallableCustom {
GDScriptFunction *function = nullptr;
Ref<GDScript> script;
uint32_t h;
GDScript::UpdatableFuncPtrElement updatable_func_ptr_element;
GDScript::UpdatableFuncPtrElement *updatable_func_ptr_element = nullptr;

Vector<Variant> captures;

Expand All @@ -72,7 +72,7 @@ class GDScriptLambdaSelfCallable : public CallableCustom {
Ref<RefCounted> reference; // For objects that are RefCounted, keep a reference.
Object *object = nullptr; // For non RefCounted objects, use a direct pointer.
uint32_t h;
GDScript::UpdatableFuncPtrElement updatable_func_ptr_element;
GDScript::UpdatableFuncPtrElement *updatable_func_ptr_element = nullptr;

Vector<Variant> captures;

Expand Down

0 comments on commit 2715117

Please sign in to comment.