使用多线程
线程
线程允许同时执行代码。它允许从主线程卸载工作。
Godot 支持线程,并提供了许多方便使用的功能。
备注
如果使用其他语言(C#、C++),它们支持的线程类可能会更容易使用。
警告
在线程中使用内置类之前,请先阅读 线程安全的 API,检查在线程中使用是否安全。
创建线程
创建线程请使用如下代码:
var thread: Thread
# The thread will start here.
func _ready():
thread = Thread.new()
# You can bind multiple arguments to a function Callable.
thread.start(_thread_function.bind("Wafflecopter"))
# Run here and exit.
# The argument is the bound data passed from start().
func _thread_function(userdata):
# Print the userdata ("Wafflecopter")
print("I'm a thread! Userdata is: ", userdata)
# Thread must be disposed (or "joined"), for portability.
func _exit_tree():
thread.wait_to_finish()
#ifndef MULTITHREADING_DEMO_H
#define MULTITHREADING_DEMO_H
#include <godot_cpp/classes/node.hpp>
#include <godot_cpp/classes/thread.hpp>
namespace godot {
class MultithreadingDemo : public Node {
GDCLASS(MultithreadingDemo, Node);
private:
Ref<Thread> worker;
protected:
static void _bind_methods();
void _notification(int p_what);
public:
MultithreadingDemo();
~MultithreadingDemo();
void demo_threaded_function();
};
} // namespace godot
#endif // MULTITHREADING_DEMO_H
#include "multithreading_demo.h"
#include <godot_cpp/classes/engine.hpp>
#include <godot_cpp/classes/os.hpp>
#include <godot_cpp/classes/time.hpp>
#include <godot_cpp/core/class_db.hpp>
#include <godot_cpp/variant/utility_functions.hpp>
using namespace godot;
void MultithreadingDemo::_bind_methods() {
ClassDB::bind_method(D_METHOD("threaded_function"), &MultithreadingDemo::demo_threaded_function);
}
void MultithreadingDemo::_notification(int p_what) {
// Prevents this from running in the editor, only during game mode. In Godot 4.3+ use Runtime classes.
if (Engine::get_singleton()->is_editor_hint()) {
return;
}
switch (p_what) {
case NOTIFICATION_READY: {
worker.instantiate();
worker->start(callable_mp(this, &MultithreadingDemo::demo_threaded_function), Thread::PRIORITY_NORMAL);
} break;
case NOTIFICATION_EXIT_TREE: { // Thread must be disposed (or "joined"), for portability.
// Wait until it exits.
if (worker.is_valid()) {
worker->wait_to_finish();
}
worker.unref();
} break;
}
}
MultithreadingDemo::MultithreadingDemo() {
// Initialize any variables here.
}
MultithreadingDemo::~MultithreadingDemo() {
// Add your cleanup here.
}
void MultithreadingDemo::demo_threaded_function() {
UtilityFunctions::print("demo_threaded_function started!");
int i = 0;
uint64_t start = Time::get_singleton()->get_ticks_msec();
while (Time::get_singleton()->get_ticks_msec() - start < 5000) {
OS::get_singleton()->delay_msec(10);
i++;
}
UtilityFunctions::print("demo_threaded_function counted to: ", i, ".");
}
然后, 你的函数将在一个单独的线程中运行, 直到它返回. 即使函数已经返回, 线程也必须收集它, 所以调用 Thread.wait_to_finish() , 它将等待线程完成(如果还没有完成), 然后妥善处理它.
警告
Creating threads is a slow operation, especially on Windows. To avoid unnecessary performance overhead, make sure to create threads before heavy processing is needed instead of creating threads just-in-time.
For example, if you need multiple threads during gameplay, you can create threads while the level is loading and only actually start processing with them later on.
Additionally, locking and unlocking of mutexes can also be an expensive operation. Locking should be done carefully; avoid locking too often (or for too long).
Mutex
并不总是支持从多个线程访问对象或数据(如果你这样做, 会导致意外行为或崩溃). 请阅读 线程安全的 API 文档, 了解哪些引擎API支持多线程访问.
在处理自己的数据或调用自己的函数时, 通常情况下, 尽量避免从不同的线程直接访问相同的数据. 你可能会遇到同步问题, 因为数据被修改后,CPU核之间并不总是更新. 当从不同线程访问一个数据时, 一定要使用 Mutex .
当调用 Mutex.lock() 时, 一个线程确保所有其他线程如果试图 锁 同一个mutex, 就会被阻塞(进入暂停状态). 当通过调用 Mutex.unlock() 来解锁该mutex时, 其他线程将被允许继续锁定(但每次只能锁定一个).
下面是一个使用 Mutex 的例子:
var counter := 0
var mutex: Mutex
var thread: Thread
# The thread will start here.
func _ready():
mutex = Mutex.new()
thread = Thread.new()
thread.start(_thread_function)
# Increase value, protect it with Mutex.
mutex.lock()
counter += 1
mutex.unlock()
# Increment the value from the thread, too.
func _thread_function():
mutex.lock()
counter += 1
mutex.unlock()
# Thread must be disposed (or "joined"), for portability.
func _exit_tree():
thread.wait_to_finish()
print("Counter is: ", counter) # Should be 2.
#ifndef MUTEX_DEMO_H
#define MUTEX_DEMO_H
#include <godot_cpp/classes/mutex.hpp>
#include <godot_cpp/classes/node.hpp>
#include <godot_cpp/classes/thread.hpp>
namespace godot {
class MutexDemo : public Node {
GDCLASS(MutexDemo, Node);
private:
int counter = 0;
Ref<Mutex> mutex;
Ref<Thread> thread;
protected:
static void _bind_methods();
void _notification(int p_what);
public:
MutexDemo();
~MutexDemo();
void thread_function();
};
} // namespace godot
#endif // MUTEX_DEMO_H
#include "mutex_demo.h"
#include <godot_cpp/classes/engine.hpp>
#include <godot_cpp/classes/time.hpp>
#include <godot_cpp/core/class_db.hpp>
#include <godot_cpp/variant/utility_functions.hpp>
using namespace godot;
void MutexDemo::_bind_methods() {
ClassDB::bind_method(D_METHOD("thread_function"), &MutexDemo::thread_function);
}
void MutexDemo::_notification(int p_what) {
// Prevents this from running in the editor, only during game mode.
if (Engine::get_singleton()->is_editor_hint()) {
return;
}
switch (p_what) {
case NOTIFICATION_READY: {
UtilityFunctions::print("Mutex Demo Counter is starting at: ", counter);
mutex.instantiate();
thread.instantiate();
thread->start(callable_mp(this, &MutexDemo::thread_function), Thread::PRIORITY_NORMAL);
// Increase value, protect it with Mutex.
mutex->lock();
counter += 1;
UtilityFunctions::print("Mutex Demo Counter is ", counter, " after adding with Mutex protection.");
mutex->unlock();
} break;
case NOTIFICATION_EXIT_TREE: { // Thread must be disposed (or "joined"), for portability.
// Wait until it exits.
if (thread.is_valid()) {
thread->wait_to_finish();
}
thread.unref();
UtilityFunctions::print("Mutex Demo Counter is ", counter, " at EXIT_TREE."); // Should be 2.
} break;
}
}
MutexDemo::MutexDemo() {
// Initialize any variables here.
}
MutexDemo::~MutexDemo() {
// Add your cleanup here.
}
// Increment the value from the thread, too.
void MutexDemo::thread_function() {
mutex->lock();
counter += 1;
mutex->unlock();
}
Semaphore
Sometimes you want your thread to work "on demand". In other words, tell it when to work and let it suspend when it isn't doing anything. For this, Semaphores are used. The function Semaphore.wait() is used in the thread to suspend it until some data arrives.
而主线程则使用 Semaphore.post() 来表示数据已经准备好被处理:
var counter := 0
var mutex: Mutex
var semaphore: Semaphore
var thread: Thread
var exit_thread := false
# The thread will start here.
func _ready():
mutex = Mutex.new()
semaphore = Semaphore.new()
exit_thread = false
thread = Thread.new()
thread.start(_thread_function)
func _thread_function():
while true:
semaphore.wait() # Wait until posted.
mutex.lock()
var should_exit = exit_thread # Protect with Mutex.
mutex.unlock()
if should_exit:
break
mutex.lock()
counter += 1 # Increment counter, protect with Mutex.
mutex.unlock()
func increment_counter():
semaphore.post() # Make the thread process.
func get_counter():
mutex.lock()
# Copy counter, protect with Mutex.
var counter_value = counter
mutex.unlock()
return counter_value
# Thread must be disposed (or "joined"), for portability.
func _exit_tree():
# Set exit condition to true.
mutex.lock()
exit_thread = true # Protect with Mutex.
mutex.unlock()
# Unblock by posting.
semaphore.post()
# Wait until it exits.
thread.wait_to_finish()
# Print the counter.
print("Counter is: ", counter)
#ifndef SEMAPHORE_DEMO_H
#define SEMAPHORE_DEMO_H
#include <godot_cpp/classes/mutex.hpp>
#include <godot_cpp/classes/node.hpp>
#include <godot_cpp/classes/semaphore.hpp>
#include <godot_cpp/classes/thread.hpp>
namespace godot {
class SemaphoreDemo : public Node {
GDCLASS(SemaphoreDemo, Node);
private:
int counter = 0;
Ref<Mutex> mutex;
Ref<Semaphore> semaphore;
Ref<Thread> thread;
bool exit_thread = false;
protected:
static void _bind_methods();
void _notification(int p_what);
public:
SemaphoreDemo();
~SemaphoreDemo();
void thread_function();
void increment_counter();
int get_counter();
};
} // namespace godot
#endif // SEMAPHORE_DEMO_H
#include "semaphore_demo.h"
#include <godot_cpp/classes/engine.hpp>
#include <godot_cpp/classes/time.hpp>
#include <godot_cpp/core/class_db.hpp>
#include <godot_cpp/variant/utility_functions.hpp>
using namespace godot;
void SemaphoreDemo::_bind_methods() {
ClassDB::bind_method(D_METHOD("thread_function"), &SemaphoreDemo::thread_function);
}
void SemaphoreDemo::_notification(int p_what) {
// Prevents this from running in the editor, only during game mode.
if (Engine::get_singleton()->is_editor_hint()) {
return;
}
switch (p_what) {
case NOTIFICATION_READY: {
UtilityFunctions::print("Semaphore Demo Counter is starting at: ", counter);
mutex.instantiate();
semaphore.instantiate();
exit_thread = false;
thread.instantiate();
thread->start(callable_mp(this, &SemaphoreDemo::thread_function), Thread::PRIORITY_NORMAL);
increment_counter(); // Call increment counter to test.
} break;
case NOTIFICATION_EXIT_TREE: { // Thread must be disposed (or "joined"), for portability.
// Set exit condition to true.
mutex->lock();
exit_thread = true; // Protect with Mutex.
mutex->unlock();
// Unblock by posting.
semaphore->post();
// Wait until it exits.
if (thread.is_valid()) {
thread->wait_to_finish();
}
thread.unref();
// Print the counter.
UtilityFunctions::print("Semaphore Demo Counter is ", get_counter(), " at EXIT_TREE.");
} break;
}
}
SemaphoreDemo::SemaphoreDemo() {
// Initialize any variables here.
}
SemaphoreDemo::~SemaphoreDemo() {
// Add your cleanup here.
}
// Increment the value from the thread, too.
void SemaphoreDemo::thread_function() {
while (true) {
semaphore->wait(); // Wait until posted.
mutex->lock();
bool should_exit = exit_thread; // Protect with Mutex.
mutex->unlock();
if (should_exit) {
break;
}
mutex->lock();
counter += 1; // Increment counter, protect with Mutex.
mutex->unlock();
}
}
void SemaphoreDemo::increment_counter() {
semaphore->post(); // Make the thread process.
}
int SemaphoreDemo::get_counter() {
mutex->lock();
// Copy counter, protect with Mutex.
int counter_value = counter;
mutex->unlock();
return counter_value;
}