我阅读了文档。现在,我有点了解 Godot 中函数的线程功能了。我或多或少成功地使用了它(但在代码中添加了更多位后,项目就崩溃了)。我也通读了线程安全 API,这对我来说是一个灰色地带。互斥体也是如此。和信号量。我不是 IT 人员,所以它更像是像我一样的解释 5)
我可以或不能使用线程的具体内容是什么?调用另一个节点的函数可以吗?或者我应该直接在该节点中放置一个新线程?
什么是互斥体?互斥体locking
有什么作用?
什么是信号量?
什么时候用什么?unlocking
我阅读了文档。现在,我有点了解 Godot 中函数的线程功能了。我或多或少成功地使用了它(但在代码中添加了更多位后,项目就崩溃了)。我也通读了线程安全 API,这对我来说是一个灰色地带。互斥体也是如此。和信号量。我不是 IT 人员,所以它更像是像我一样的解释 5)
我可以或不能使用线程的具体内容是什么?调用另一个节点的函数可以吗?或者我应该直接在该节点中放置一个新线程?
什么是互斥体?互斥体locking
有什么作用?
什么是信号量?
什么时候用什么?unlocking
好像你5岁一样?好吧...在代码的土地上...
单线程与多线程
代码的执行从一条指令流向下一条指令。有时它会跳过一些条件部分(当有一个
if
或一个match
语句时),或者它进入一个循环(当有一个for
或一个while
语句时)。但无论如何,它总是一次执行一条指令。这就是我们所说的“单线程”执行。但有一天,“多线程”来到了代码之地……
当有多个线程时,每个线程都有自己的执行流程。当一个线程执行一条指令时,另一个线程执行另一条指令。
这可能很好,因为它可能允许代码同时完成更多工作。但这也可能很糟糕,因为一个线程正在做的事情可能会干扰其他线程。
因此必须小心,以免线缠结......
在主线程上
即使有多线程,仍然有主线程,这与单线程执行时的情况相同。而且很多东西都绑定到主线程(特别是UI绑定到创建窗口的线程,第一个窗口是由主线程创建的)。尝试从另一个线程访问它们会导致问题......从平凡到令人讨厌。
延迟执行
我们的主要工具是延迟执行,因为它总是发生在主线程上,无论哪个线程请求它。而且由于线程一次只做一件事,所以不会有麻烦。
Deferred 调用是其他线程向主线程传递结果的好方法。但是用延迟执行来完成所有事情都会浪费线程的潜力。
由于延迟调用稍后会在主线程上发生,因此从辅助线程的角度来看,延迟调用是即发即忘(辅助线程无法等待它,或从中获取结果,它会继续执行马上)。
您可以使用辅助线程的延迟调用来提供它
Callable
。但如果主线程调用Callable
,它将在主线程中运行。我告诉你,这不是一个解决方法。我想让你想象一下这样的场景:你有一些复杂的计算,或者你正在做一些大型的IO操作(例如读取一个大文件),这将需要一些时间......
如果您在主线程中执行此操作,它将忙于执行此操作,并且不会更新游戏/应用程序的其余部分。结果,用户将看到游戏/应用程序冻结。
为了避免这种情况,您可以使用辅助
Thread
来完成工作。但最后你想显示一个结果,或者你想在它运行时显示一些进度条。为此,辅助线程可以使用延迟调用。抢占式线程
为了能够让多个线程处理相同的值,我们需要更好地理解线程。值得注意的是,线程是抢占式的(通常),这意味着线程可能随时挂起和恢复。有时在您的控制之下,有时超出您的控制。因此,无论线程正在做什么,都可能暂时处于未捕获状态。
有些指令是原子的,这意味着它们不能被细分,因此线程要么执行它们,要么不执行它们。他们不能半途而废。遗憾的是,这可能取决于数据量和平台(有时读取或写入不是单个操作,而是必须分块完成)……因此,除非明确说明某些内容保证是原子的,否则不要依赖于此。
因此,可能发生的事情之一是,一个线程可能正在写入一个变量,但只写入了一半,而另一个线程像这样读取它,并且......哦不,未定义的行为!混乱!崩溃!
(通常)不是原子的事情是递增变量。它可以分解为读取变量的值,计算增量值,然后写入它。
在这种情况下,一个线程可能会读取该值,计算增量值......并被抢占!另一个线程进来,读取该值,计算增量值并将其写入。现在,第一个线程恢复并写入它计算的值...但会覆盖第二个线程的工作!第一个线程不知道第二个线程更改了值!结果,该值仅增加一次,而不是两次。
这称为ABA 问题,另请参阅检查时间到使用时间。这也是竞争条件的一个例子。
锁具
如果您需要辅助线程来等待某些内容,请使用锁(互斥锁或信号量)。
我们希望其他线程等到我们完成计算和写入值后再读取结果。并且还要阻止他们编写任何可能扰乱我们在线程中所做的工作的内容。
我们将想要控制线程访问的地方称为“关键部分”。
我们有相应的工具!
互斥体
线程控制的一种工具是互斥体!这代表相互排斥!当一个线程获取互斥锁时,它会锁定它,但一次只有一个线程可以使用它运行,所有其他线程将被挂起,直到使用互斥锁运行的线程释放它。
但有时您不希望线程等到互斥锁被释放,在这种情况下,您可以让它们尝试锁定互斥锁,如果失败,它们可以自由地执行其他操作。只是不要让他们在关键部分胡作非为。
有时,您有一个线程生成值供多个其他线程使用。在这种情况下,线程读取值时的临界区不是互斥的。
信号
线程控制的另一个工具是信号量。当您有一个线程生成值而其他线程使用它们时,这很有用。当然,您不希望线程在读取值之前进入消耗值。
您要做的就是让消费者线程等待信号量,然后生产者线程可以向信号量发出信号以让线程通过。信号量将允许与生产者线程信号一样多的线程通过。
*遗憾的是,我们在《戈多》中没有更多的内容。我希望有奇特的读写锁,或者原子增量,或者互锁操作,或者......
重新排序
为什么我们不直接使用 a
bool
来指示结果是否准备好呢?另一个线程可以读取它,并且只有在true
...的情况下才可以访问它,因为恶作剧!首先,虽然 Godot 不会对 GDScript 执行此操作,但用于编译 Godot 的 C++ 编译器可能会重新排序指令,只要它确信结果是相同的(并且这是静态检查的,不考虑其他线程)。
一般来说,C++ 编译器可以自由地编译他们想要的代码,只要结果就像代码执行程序员编写的那样(这是允许他们引入优化的自由,例如删除一条写入变量的指令显然没有人读)。
其次,即使编译器不重新排序指令,CPU 仍然可能会重新排序。虽然这种情况在台式计算机中使用的 CPU 上很少见,但在其他架构上并不多见。
第三,CPU 有缓存。特别是现代多核 CPU,每个核心都有高速缓存。他们将从缓存中读取数据(如果有),而不是从 RAM 中读取。
因此,一个线程可能无法立即看到另一个线程所做的更改或以相同的顺序进行更改。因此,即使他们看到设置
bool
为true
,他们仍然可能会看到其他变量的过时值。为了防止重新排序,我们使用称为“内存屏障”的东西(以及类似的机制,用于实现锁)……但是,讨论这一点没有什么意义,因为我们没有办法从 GDScript 中生成 then。我们只能使用
Mutex
andSemaphore
and...线程组
我仍然需要告诉你有关新发明的线程组的信息。如果将 a 的线程组设置
Node
为PROCESS_THREAD_GROUP_MAIN_THREAD
它将在主线程中运行。如果将其设置为PROCESS_THREAD_GROUP_SUB_THREAD
,它将运行一个新线程。如果您将其设置为PROCESS_THREAD_GROUP_INHERIT
它将在父级的同一线程中运行。这允许您定义一组
Node
将在单独的线程上操作的 s,只要它们只在它们之间工作,就不会出现问题,因为它们都在同一个线程上工作。您可以使用延迟调用将结果提供给主线程。我们还有一组函数,
Node
旨在确保与子线程的通信安全(thread
类中名称中的方法Node
)。它们内置了内存屏障。信号和等待
当您等待信号时(您可以从辅助线程执行此操作,但请不要这样做),调用将返回。也就是说,执行流程将退出该方法。并且它将返回并存储执行流离开的位置的对象,以便稍后在发出等待的信号时恢复它。
然后当信号发出时,主线程将获取代表执行位置的对象并从那里继续!因此,等待辅助线程会导致线程切换。
注意:这种机制将来可能会改变,因为线程组仍处于实验阶段,并且可能会扩展以涵盖这种情况。
关于线程安全
哦,线程安全!这意味着从任何线程直接调用某些东西都是安全的。或者换句话说,它是在内部编写的,假设它可能会同时从多个线程调用,并且采取预防措施来防止这种情况导致任何意外的行为或崩溃......简单地说:开发人员照顾了多线程,所以你不必这样做。
而且,正如文档所指出的,场景树不是线程安全的。这使得场景树的任何操作默认成为关键部分。
物理笔记
另一件需要考虑的事情是,Godot 可能会使用单独的物理线程(您可以从项目设置中启用)。这也适用于您
Node
从物理学中得到的一些调用。