複数のスレッドの使用
スレッド
スレッドはコードの同時実行を可能にします。これにより、メインスレッドの負荷を軽減します。
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() を呼び出し、(まだ終了していない場合は)スレッドが終了するまで待機し、適切に破棄します。
警告
スレッドの作成は、特に Windows では低速な操作です。不要なパフォーマンスのオーバーヘッドを回避するには、ジャストインタイムでスレッドを作成するのではなく、負荷の高い処理が必要になる前に必ずスレッドを作成するようにしてください。
たとえば、ゲームプレイ中に複数のスレッドが必要な場合は、レベルの読み込み中にスレッドを作成し、後で実際に処理を開始することができます。
さらに、ミューテックスのロックとロック解除もコストのかかる操作になる可能性があります。ロックは慎重に行う必要があり、ロックを頻繁に (または長時間) 行わないようにしてください。
ミューテックス
複数のスレッドからのオブジェクトまたはデータへのアクセスは必ずしもサポートされているわけではありません (これを行うと、予期しない動作やクラッシュが発生します)。どのエンジン API が複数のスレッド アクセスをサポートしているかを理解するには、スレッドセーフAPI ドキュメントをお読みください。
独自のデータを処理するとき、または独自の関数を呼び出すときは、原則として、異なるスレッドから直接同じデータにアクセスしないようにしてください。データは変更時にCPUコア間で常に更新されないため、同期の問題が発生する可能性があります。異なるスレッドからデータにアクセスするときは、常にMutex を使用します。
Mutex.lock() を呼び出すと、スレッドは、同じミューテックスをロックしようとする、他の全てのスレッドをブロックされた(休止した)状態にします。Mutex.unlock() を呼び出してmutexのロックを解除すると、新たに他のスレッドがロックを続行できます(ただし、一度に1つのスレッドのみ)。
以下は、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();
}
セマフォ
スレッドを「オンデマンド」で動作させたい場合があります。言い換えれば、いつ作業するかを伝え、何もしていないときに休止させます。このために、Semaphores が使用されます。関数 Semaphore.wait() はスレッドで使用され、データが到着するまでスレッドを休止します。
それに対し、メインスレッドは 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;
}