Skip to content

Commit 1ec3da8

Browse files
committedMar 10, 2025
Add ability to suspend threads until a signal is called
For GDScript users that run code on threads, it often happens they want to `await` until something happens, the continue the thread execution. Unfortunately, signals will most likely execute on the main thread, hence this means that the code being run on the thread will continue running on the main thread by default. It has been discussed whether await should be smart about this and simply suspend the thread if running on one. The problem with this is, that users may also be willing to emit the signal that will resume from the thread itself and, at the time of awaiting, there is **no way for the interpreter to know on which thread the function will be resumed**. Additionally, suspending the thread is a very different operation than awaiting. Awaiting saves the local function stack and returns immediately, while suspending stops the whole thread until another resumes it. Mixing and matching both seems, ultimately, undesired as they are not the same. To solve this, a new utility function is added by this pull request, which suspends a thread until a signal is emitted (no matter in which other thread). It works like this. ```GDScript # Suspends a thread until a button is pressed. thread_suspend( button.pressed ) ``` If code must do this and can run on both the main thread or a dedicated thread, it can do as follows: ```GDScript # Suspends a thread until a button is pressed. if (Thread.is_main_thread()): await button.pressed else: thread_suspend( button.pressed ) ``` For this, the `Thread.is_main_thread()` function (already existing in the internal Thread object) has been exposed to the engine API. **Q**: Are you sure this can't be done with await transparently? **A**: No, please read again. There is no way for await to know beforehand how the function will be resumed. Only the user knows. **Q**: Does it not make more sense to have an `await_thread` or something where the user will pass the argument? **A**: I think its better to have a dedicated utility function for this. Not only the intention is more explicit, but additionally, as this makes it non exclusive to GDScript. It can be used from other languages that use the engine.
1 parent cae3d72 commit 1ec3da8

File tree

6 files changed

+98
-0
lines changed

6 files changed

+98
-0
lines changed
 

‎core/core_bind.cpp

+5
Original file line numberDiff line numberDiff line change
@@ -1391,6 +1391,10 @@ bool Thread::is_alive() const {
13911391
return running.is_set();
13921392
}
13931393

1394+
bool Thread::is_main_thread() {
1395+
return ::Thread::is_main_thread();
1396+
}
1397+
13941398
Variant Thread::wait_to_finish() {
13951399
ERR_FAIL_COND_V_MSG(!is_started(), Variant(), "Thread must have been started to wait for its completion.");
13961400
thread.wait_to_finish();
@@ -1413,6 +1417,7 @@ void Thread::_bind_methods() {
14131417
ClassDB::bind_method(D_METHOD("wait_to_finish"), &Thread::wait_to_finish);
14141418

14151419
ClassDB::bind_static_method("Thread", D_METHOD("set_thread_safety_checks_enabled", "enabled"), &Thread::set_thread_safety_checks_enabled);
1420+
ClassDB::bind_static_method("Thread", D_METHOD("is_main_thread"), &Thread::is_main_thread);
14161421

14171422
BIND_ENUM_CONSTANT(PRIORITY_LOW);
14181423
BIND_ENUM_CONSTANT(PRIORITY_NORMAL);

‎core/core_bind.h

+1
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,7 @@ class Thread : public RefCounted {
453453
bool is_started() const;
454454
bool is_alive() const;
455455
Variant wait_to_finish();
456+
static bool is_main_thread();
456457

457458
static void set_thread_safety_checks_enabled(bool p_enabled);
458459
};

‎core/variant/variant_utility.cpp

+78
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
#include "core/io/marshalls.h"
3434
#include "core/object/ref_counted.h"
3535
#include "core/os/os.h"
36+
#include "core/os/semaphore.h"
3637
#include "core/templates/oa_hash_map.h"
3738
#include "core/templates/rid.h"
3839
#include "core/templates/rid_owner.h"
@@ -1234,6 +1235,81 @@ bool VariantUtilityFunctions::is_same(const Variant &p_a, const Variant &p_b) {
12341235
return p_a.identity_compare(p_b);
12351236
}
12361237

1238+
class CallableCustomSuspend : public CallableCustom {
1239+
Semaphore *semaphore = nullptr;
1240+
Variant *return_value = nullptr;
1241+
1242+
// Never really going to execute since disconnection is automatic.
1243+
static bool _equal_func(const CallableCustom *p_a, const CallableCustom *p_b) {
1244+
const CallableCustomSuspend *A = static_cast<const CallableCustomSuspend *>(p_a);
1245+
const CallableCustomSuspend *B = static_cast<const CallableCustomSuspend *>(p_b);
1246+
1247+
return A->semaphore == B->semaphore;
1248+
}
1249+
1250+
// Never really going to execute since disconnection is automatic.
1251+
static bool _less_func(const CallableCustom *p_a, const CallableCustom *p_b) {
1252+
const CallableCustomSuspend *A = static_cast<const CallableCustomSuspend *>(p_a);
1253+
const CallableCustomSuspend *B = static_cast<const CallableCustomSuspend *>(p_b);
1254+
1255+
return A->semaphore < B->semaphore;
1256+
}
1257+
1258+
public:
1259+
//for every type that inherits, these must always be the same for this type
1260+
virtual uint32_t hash() const override {
1261+
return size_t(semaphore);
1262+
}
1263+
1264+
virtual String get_as_text() const override {
1265+
return "SemaphoreCallable";
1266+
}
1267+
1268+
virtual CompareEqualFunc get_compare_equal_func() const override {
1269+
return _equal_func;
1270+
}
1271+
1272+
virtual CompareLessFunc get_compare_less_func() const override {
1273+
return _less_func;
1274+
}
1275+
1276+
virtual ObjectID get_object() const override {
1277+
return ObjectID();
1278+
}
1279+
1280+
virtual void call(const Variant **p_arguments, int p_argcount, Variant &r_return_value, Callable::CallError &r_call_error) const override {
1281+
semaphore->post();
1282+
if (p_argcount == 1) { // If passed one argument, will be returned.
1283+
if (return_value) {
1284+
*return_value = *p_arguments[0];
1285+
}
1286+
} else if (p_argcount > 1) {
1287+
r_call_error.error = Callable::CallError::CALL_ERROR_TOO_MANY_ARGUMENTS;
1288+
r_call_error.argument = p_argcount;
1289+
r_call_error.expected = 1;
1290+
return;
1291+
}
1292+
1293+
r_call_error.error = Callable::CallError::CALL_OK;
1294+
}
1295+
1296+
CallableCustomSuspend(Semaphore *p_semaphore, Variant *r_return_value) {
1297+
semaphore = p_semaphore;
1298+
return_value = r_return_value;
1299+
}
1300+
};
1301+
1302+
Variant VariantUtilityFunctions::thread_suspend(Signal p_resume) {
1303+
Semaphore semaphore;
1304+
Variant return_value;
1305+
1306+
p_resume.connect(Callable(memnew(CallableCustomSuspend(&semaphore, &return_value))), Object::CONNECT_ONE_SHOT);
1307+
1308+
semaphore.wait();
1309+
1310+
return return_value;
1311+
}
1312+
12371313
#ifdef DEBUG_METHODS_ENABLED
12381314
#define VCALLR *ret = p_func(VariantCasterAndValidate<P>::cast(p_args, Is, r_error)...)
12391315
#define VCALL p_func(VariantCasterAndValidate<P>::cast(p_args, Is, r_error)...)
@@ -1855,6 +1931,8 @@ void Variant::_register_variant_utility_functions() {
18551931
FUNCBINDR(rid_from_int64, sarray("base"), Variant::UTILITY_FUNC_TYPE_GENERAL);
18561932

18571933
FUNCBINDR(is_same, sarray("a", "b"), Variant::UTILITY_FUNC_TYPE_GENERAL);
1934+
1935+
FUNCBINDR(thread_suspend, sarray("on_signal"), Variant::UTILITY_FUNC_TYPE_GENERAL);
18581936
}
18591937

18601938
void Variant::_unregister_variant_utility_functions() {

‎core/variant/variant_utility.h

+1
Original file line numberDiff line numberDiff line change
@@ -152,4 +152,5 @@ struct VariantUtilityFunctions {
152152
static uint64_t rid_allocate_id();
153153
static RID rid_from_int64(uint64_t p_base);
154154
static bool is_same(const Variant &p_a, const Variant &p_b);
155+
static Variant thread_suspend(Signal p_resume);
155156
};

‎doc/classes/@GlobalScope.xml

+7
Original file line numberDiff line numberDiff line change
@@ -1390,6 +1390,13 @@
13901390
[/codeblock]
13911391
</description>
13921392
</method>
1393+
<method name="thread_suspend">
1394+
<return type="Variant" />
1395+
<param index="0" name="on_signal" type="Signal" />
1396+
<description>
1397+
Suspend the caller thread until the signal passed as argument is emitted.
1398+
</description>
1399+
</method>
13931400
<method name="type_convert">
13941401
<return type="Variant" />
13951402
<param index="0" name="variant" type="Variant" />

‎doc/classes/Thread.xml

+6
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@
3030
To check if a [Thread] is joinable, use [method is_started].
3131
</description>
3232
</method>
33+
<method name="is_main_thread" qualifiers="static">
34+
<return type="bool" />
35+
<description>
36+
Returns [code]true[/code] if the function is called from the main thread of the engine. This is the thread that runs game code by default.
37+
</description>
38+
</method>
3339
<method name="is_started" qualifiers="const">
3440
<return type="bool" />
3541
<description>

0 commit comments

Comments
 (0)