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.
Checking the stable version of the documentation...
线程安全的 API
线程
线程用于在 CPU 和核心之间平衡处理能力。Godot 支持多线程,但并非整个引擎都支持。
以下是一系列在 Godot 不同区域使用多线程的方式。
全局作用域
大多数全局作用域单例默认都是线程安全的,支持从线程访问服务器。然而,对于渲染和物理服务器,必须先在项目设置中启用线程安全操作。
这使得单例非常适合在服务器中创建数万个实例,并通过线程来控制它们。当然,因为这是直接使用服务器而非通过场景树,所以需要编写更多代码。
场景树
与活动场景树交互不是线程安全的。在线程之间发送数据时,请确保使用互斥锁。如果要从线程调用函数或设置属性,可以使用 call_deferred 或 set_deferred:
# Unsafe:
node.add_child(child_node)
# Safe:
node.add_child.call_deferred(child_node)
// Unsafe:
node.AddChild(childNode);
// Safe:
node.CallDeferred(Node.MethodName.AddChild, childNode);
但是,在活动场景树外创建场景块(以树形结构排列的节点)是没问题的。这样,可以在线程中构建或实例化部分场景,然后将其添加到主线程中:
var enemy_scene = load("res://enemy_scene.scn")
var enemy = enemy_scene.instantiate()
enemy.add_child(weapon) # Set a weapon.
world.add_child.call_deferred(enemy)
PackedScene enemyScene = GD.Load<PackedScene>("res://EnemyScene.scn");
Node enemy = enemyScene.Instantiate<Node>();
enemy.AddChild(weapon);
world.CallDeferred(Node.MethodName.AddChild, enemy);
不过,这仅在只有单个线程加载数据时才有用。从多个线程加载或创建场景块或许能正常工作,但这要冒着资源(只会在 Godot 中加载一次)被多个线程修改的风险,可能导致意外行为或崩溃。
只有在你真正知道自己在做什么,并且确信一个资源没有被多个线程使用或设置时,才可以使用多个线程来生成场景数据。否则,直接使用服务器 API(它是完全线程安全的)而不接触场景或资源会更安全。
渲染
默认情况下,实例化能够渲染 2D 或 3D 内容的节点(如 Sprite2D 或 MeshInstance3D)不是线程安全的。要让渲染驱动在单独的线程上运行,请将渲染 > 驱动 > 线程模型 项目设置设为 Separate。
请注意,Separate 线程模型有若干已知问题,因此可能无法适用于所有场景。
警告
你应该避免在其他线程上调用涉及与 GPU 直接交互的函数,例如创建新纹理或修改、检索图像数据。这些操作可能会导致性能停滞,因为它们需要与 RenderingServer 同步,原因是数据需要传输到 GPU 或在 GPU 上更新。
物理
默认情况下,物理模拟不是线程安全的。要在单独的线程上运行物理服务器(使其变得线程安全),请启用以下项目设置:
PhysicsServer2D:物理 > 2D > 在独立的线程上运行。
PhysicsServer3D:物理 > 3D > 在独立的线程上运行。
GDScript 数组和字典
在 GDScript 中,从多个线程读取和写入元素是可以的,但任何改变容器大小的操作(调整大小、添加或删除元素)都需要锁定互斥锁。
资源
不支持从多个线程修改同一个唯一资源。不过,支持在多个线程上处理该资源的引用。因此,在线程上加载资源也是支持的——场景、纹理、网格等可以在单个线程上加载和操作,然后添加到主线程上的活动场景中。此处的限制如上所述:必须注意不要同时从多个线程加载相同的资源。因此,最简单的方法是使用一个线程来加载和修改资源,然后使用主线程来添加它们。