Usando múltiples hilos

Hilos

Los hilos permiten la ejecución simultánea de código, lo que permite descargar trabajo del hilo principal.

Godot admite hilos y proporciona muchas funciones útiles para utilizarlos.

Nota

Si estás utilizando otros lenguajes como C# o C++, puede ser más fácil utilizar las clases de hilos que admiten esos lenguajes.

Advertencia

Antes de utilizar una clase incorporada en un hilo, lee APIs thread safe primero para comprobar si se puede utilizar de forma segura en un hilo.

Creando un hilo

Crear un hilo es muy simple, sólo usa el siguiente código:

var thread

# The thread will start here.
func _ready():
    thread = Thread.new()
    # Third argument is optional userdata, it can be any variable.
    thread.start(self, "_thread_function", "Wafflecopter")


# Run here and exit.
# The argument is the userdata passed from start().
# If no argument was passed, this one still needs to
# be here and it will be null.
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()

Tu función se ejecutará en un hilo separado hasta que regrese. Incluso si la función ya ha regresado, el hilo debe recogerla, por lo que debes llamar a Thread.wait_to_finish(), lo cual esperará hasta que el hilo haya terminado (si aún no ha terminado) y luego lo desechará correctamente.

Mutexes*

Acceder a objetos o datos desde múltiples hilos no siempre es compatible (si lo haces, puede provocar comportamientos inesperados o fallos). Lee la documentación de APIs thread safe para comprender qué APIs del motor admiten el acceso desde múltiples hilos de forma segura.

Cuando proceses tus propios datos o llames a tus propias funciones, como regla general, trata de evitar acceder directamente a los mismos datos desde diferentes hilos. Puedes encontrar problemas de sincronización, ya que los datos no siempre se actualizan entre los núcleos de la CPU cuando se modifican. Siempre utiliza un Mutex al acceder a un fragmento de datos desde diferentes hilos.

Al llamar a Mutex.lock(), un hilo garantiza que todos los demás hilos se bloquearán (colocados en estado suspendido) si intentan bloquear el mismo mutex. Cuando se desbloquea el mutex llamando a Mutex.unlock(), los otros hilos podrán continuar con el bloqueo (pero solo uno a la vez).

Aquí hay un ejemplo del uso de un Mutex:

var counter = 0
var mutex
var thread


# The thread will start here.
func _ready():
    mutex = Mutex.new()
    thread = Thread.new()
    thread.start(self, "_thread_function")

    # Increase value, protect it with Mutex.
    mutex.lock()
    counter += 1
    mutex.unlock()


# Increment the value from the thread, too.
func _thread_function(userdata):
    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.

Semáforos

A veces quieres que tu hilo trabaje "bajo demanda". En otras palabras, le indicas cuándo debe trabajar y lo suspendes cuando no está haciendo nada. Para esto, se utilizan los Semáforos. La función Semaphore.wait() se utiliza en el hilo para suspenderlo hasta que llegue algún dato.

El hilo principal, en cambio, utiliza Semaphore.post() para indicar que los datos están listos para ser procesados:

var counter = 0
var mutex
var semaphore
var 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(self, "_thread_function")


func _thread_function(userdata):
    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)