From 2f39d8ebef955bd32ad1816a24463cc829bf50c9 Mon Sep 17 00:00:00 2001 From: Juan Date: Wed, 16 Apr 2025 15:28:41 +0200 Subject: [PATCH] Add thread safety to Object signals * It turns out the majority of this work was done already by AThousandShips as part of #89451. This allows to do lock-less emitting of signals. * This means, that only the signal map needs to be protected, making the task simple and without risk of deadlocks, or affecting performance. * Objects can choose to not protect signals for performance (as example Node uses thread guards for protection, so these signals are not thread safe). --- core/object/object.cpp | 180 +++++++++++++++++++++++++++-------------- core/object/object.h | 5 +- doc/classes/Object.xml | 1 + scene/main/node.h | 2 + 4 files changed, 128 insertions(+), 60 deletions(-) diff --git a/core/object/object.cpp b/core/object/object.cpp index 852d877ff19..2753b29b29f 100644 --- a/core/object/object.cpp +++ b/core/object/object.cpp @@ -65,6 +65,23 @@ struct _ObjectDebugLock { #endif +struct _ObjectSignalLock { + Mutex *mutex; + _ObjectSignalLock(const Object *const p_obj) { + mutex = p_obj->signal_mutex; + if (mutex) { + mutex->lock(); + } + } + ~_ObjectSignalLock() { + if (mutex) { + mutex->unlock(); + } + } +}; + +#define OBJ_SIGNAL_LOCK _ObjectSignalLock _signal_lock(this); + PropertyInfo::operator Dictionary() const { Dictionary d; d["name"] = name; @@ -255,6 +272,9 @@ void Object::_initialize() { } void Object::_postinitialize() { + if (_uses_signal_mutex()) { + signal_mutex = memnew(Mutex); + } notification(NOTIFICATION_POSTINITIALIZE); } @@ -1123,6 +1143,9 @@ void Object::get_meta_list(List *p_list) const { void Object::add_user_signal(const MethodInfo &p_signal) { ERR_FAIL_COND_MSG(p_signal.name.is_empty(), "Signal name cannot be empty."); ERR_FAIL_COND_MSG(ClassDB::has_signal(get_class_name(), p_signal.name), vformat("User signal's name conflicts with a built-in signal of '%s'.", get_class_name())); + + OBJ_SIGNAL_LOCK + ERR_FAIL_COND_MSG(signal_map.has(p_signal.name), vformat("Trying to add already existing signal '%s'.", p_signal.name)); SignalData s; s.user = p_signal; @@ -1130,6 +1153,8 @@ void Object::add_user_signal(const MethodInfo &p_signal) { } bool Object::_has_user_signal(const StringName &p_name) const { + OBJ_SIGNAL_LOCK + if (!signal_map.has(p_name)) { return false; } @@ -1137,6 +1162,8 @@ bool Object::_has_user_signal(const StringName &p_name) const { } void Object::_remove_user_signal(const StringName &p_name) { + OBJ_SIGNAL_LOCK + SignalData *s = signal_map.getptr(p_name); ERR_FAIL_NULL_MSG(s, "Provided signal does not exist."); ERR_FAIL_COND_MSG(!s->removable, "Signal is not removable (not added with add_user_signal)."); @@ -1183,46 +1210,53 @@ Error Object::emit_signalp(const StringName &p_name, const Variant **p_args, int return ERR_CANT_ACQUIRE_RESOURCE; //no emit, signals blocked } - SignalData *s = signal_map.getptr(p_name); - if (!s) { -#ifdef DEBUG_ENABLED - bool signal_is_valid = ClassDB::has_signal(get_class_name(), p_name); - //check in script - ERR_FAIL_COND_V_MSG(!signal_is_valid && !script.is_null() && !Ref