Stateful Goroutines in GDScript

Our example demonstrates how to manage state using GDScript’s built-in threading and signal mechanisms. This approach aligns with GDScript’s ideas of sharing data through communication.

extends Node

class ReadOp:
    var key: int
    var result: int

class WriteOp:
    var key: int
    var value: int
    var done: bool

var state = {}
var read_ops = 0
var write_ops = 0

signal read_requested(op)
signal write_requested(op)

func _ready():
    randomize()
    
    # Start the state management thread
    var thread = Thread.new()
    thread.start(self, "_manage_state")
    
    # Start read operations
    for i in range(100):
        var read_thread = Thread.new()
        read_thread.start(self, "_perform_reads")
    
    # Start write operations
    for i in range(10):
        var write_thread = Thread.new()
        write_thread.start(self, "_perform_writes")
    
    # Let the operations run for a second
    yield(get_tree().create_timer(1.0), "timeout")
    
    # Report the results
    print("readOps:", read_ops)
    print("writeOps:", write_ops)

func _manage_state():
    while true:
        var op = yield(self, "read_requested")
        if op is ReadOp:
            op.result = state.get(op.key, 0)
        
        op = yield(self, "write_requested")
        if op is WriteOp:
            state[op.key] = op.value
            op.done = true

func _perform_reads():
    while true:
        var read = ReadOp.new()
        read.key = randi() % 5
        emit_signal("read_requested", read)
        yield(get_tree(), "idle_frame")  # Wait for the result
        read_ops += 1
        yield(get_tree().create_timer(0.001), "timeout")

func _perform_writes():
    while true:
        var write = WriteOp.new()
        write.key = randi() % 5
        write.value = randi() % 100
        emit_signal("write_requested", write)
        yield(get_tree(), "idle_frame")  # Wait for the operation to complete
        write_ops += 1
        yield(get_tree().create_timer(0.001), "timeout")

In this example, we use GDScript’s threading and signals to manage shared state. The state is owned by a single thread, which responds to read and write requests sent via signals.

We start by defining ReadOp and WriteOp classes to encapsulate our read and write operations.

The _manage_state function runs in its own thread and owns the state dictionary. It continuously waits for read or write requests and processes them.

We then start 100 threads to perform read operations and 10 threads to perform write operations. Each operation is performed by emitting a signal with the appropriate operation object.

After letting the operations run for a second, we print out the total number of read and write operations performed.

This approach ensures that the state is never corrupted by concurrent access, as all access is managed by a single thread. It demonstrates how to use GDScript’s built-in concurrency features to safely manage shared state.

Running this program will show the number of read and write operations completed in one second, which should be in the tens of thousands.

This method might be more complex than using mutexes, but it can be useful in scenarios involving multiple channels or when managing multiple mutexes would be error-prone. Choose the approach that feels most natural and helps you ensure the correctness of your program.