Attention: Here be dragons

This is the latest (unstable) version of this documentation, which may document features not available in or compatible with released stable versions of Godot.

使用多线程

参见

有关 C++ 中多线程原语的列表,请参阅多线程 / 并发

线程

线程允许同时执行代码,可以将工作从主线程转移出去。

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()

然后,你的函数将在单独的线程中运行,直到它返回。即使函数已经返回,也必须收集线程,因此请调用 Thread.wait_to_finish(),它将等待线程完成(如果尚未完成),然后妥善清理它。

警告

创建线程是一个较慢的操作,尤其是在 Windows 上。为了避免不必要的性能开销,请确保在需要大量处理之前创建线程,而不是临时创建线程。

例如,如果你在游戏过程中需要多个线程,可以在加载关卡时创建线程,稍后再实际开始使用它们进行处理。

此外,互斥锁的锁定和解锁操作也可能是一项开销较大的操作。锁定操作应当谨慎进行,避免过于频繁地锁定(或锁定时间过长)。

互斥锁

从多个线程访问对象或数据并不总是受支持的(如果你这样做,会导致意外行为或崩溃)。请阅读线程安全的 API文档,了解哪些引擎 API 支持多线程访问。

在处理自己的数据或调用自己的函数时,通常情况下,尽量避免从不同的线程直接访问相同的数据。你可能会遇到同步问题,因为数据被修改后,并不总是在 CPU 核心之间更新。当从不同线程访问一块数据时,一定要使用 Mutex(互斥锁)。

当调用 Mutex.lock() 时,线程会确保所有其他试图锁定同一互斥锁的线程被阻塞(进入暂停状态)。当通过调用 Mutex.unlock() 来解锁互斥锁时,其他线程将被允许继续锁定(但每次只能有一个线程锁定成功)。

下面是一个使用 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.

信号量

有时你希望你的线程“按需”工作。也就是说,希望告诉它何时工作,并在空闲时让其挂起。为此,可以使用 Semaphore(信号量)。可以在线程中调用函数 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)