在后台线程上执行 UI

Doing UI on a background thread

线程状态的 SDL 文档:

NOTE: You should not expect to be able to create a window, render, or receive events on any thread other than the main one.

glfwCreateWindow 的 glfw 文档指出:

Thread safety: This function must only be called from the main thread.

我已经从试图 运行 在第二个线程上 windowing 函数的人那里了解到有关 glut 库的问题。

我可以继续这些例子,但我想你已经明白我要表达的意思了。许多跨平台库不允许您在后台线程上创建 window。

现在,我提到的两个库都是为 OpenGL 设计的,我知道 OpenGL 不是为多线程设计的,您不应该在多线程上进行渲染。没关系。我不明白的是为什么渲染线程(完成所有渲染的单个线程)必须是应用程序的主要线程

据我所知,Windows、Linux 和 MacOS 都没有对哪些线程可以创建 windows 施加任何限制。我知道 windows 对创建它们的线程有亲和力(只有那个线程可以接收它们的输入,等等);但该线程仍然不需要成为主要线程。

所以,我有三个问题:

  1. 为什么这些库会施加这样的限制?是不是有什么不起眼的操作系统要求所有windows都在主线程创建,所有操作系统都要为此付出代价? (还是我听错了?)
  2. 为什么我们强加了您不应该在后台线程上执行的操作 UI?无论如何,threads 与 windowing 有什么关系?将您的逻辑绑定到 特定线程 不是一个糟糕的抽象吗?
  3. 如果这是我们所拥有的并且无法摆脱它,我该如何克服这个限制?我是否创建一个 ThreadManager class 并将主线程交给它,以便它可以安排需要在主线程中完成的工作以及可以在后台线程中完成的工作?

如果有人能阐明这个话题,那就太好了。我看到的所有建议都是在主线程上同时执行 input 和 UI 。但是,如果没有技术原因不能这样做,那只是一个任意限制。

PS:请注意,我正在寻找跨平台解决方案。如果找不到,我会坚持在主线程上做UI。

  1. 这些框架中的任何一个实际上都不太可能关心哪个线程是 'main thread',即调用代码入口点的线程。真正的限制是你必须在初始化框架的线程上完成所有 UI 工作,即在你的情况下调用 SDL_Init 的线程。您通常会在主线程中执行此操作。为什么不呢?

  2. 多线程的代码很难写,也很难理解,在UI工作中,引入多线程使得事情发生的时候很难推理。 UI 是一个非常有状态的东西,当你写 UI 代码时,你通常需要非常清楚已经发生了什么以及接下来会发生什么——这些事情通常是当涉及多线程时未定义。此外,用户速度很慢,因此在正常情况下,UI 的多线程对于性能来说并不是必需的。由于这一切,使 UI 框架线程安全通常不被认为是有益的。 (渲染管道的多线程计算密集型部分是另一回事)

  3. 单线程 UI 框架有某种类型的调度程序,您可以使用它来排队应该在主线程下次有时间时发生的活动。在 SDL 中,您为此使用 SDL_PushEvent。您可以从任何线程调用它。

虽然我不太了解 MacOS/iOS 的最新版本,但截至 2020 年,Apple UIKit 和 AppKit 不是 线程安全的。只有一个线程可以安全地更改 UI 个对象,除非你遇到很多麻烦,否则它将成为主线程。即使您不厌其烦地关闭 window 管理器连接等等,您仍然会以一个线程只执行 UI 结束。所以限制仍然适用于至少一个主要系统。

虽然从任何其他线程直接修改 window 的内容可能不安全,但您可以从您喜欢的任何线程对屏幕外位图图像进行软件渲染,只要您愿意。然后将完成的图像交给主线程进行渲染。 (可能 是你不使用跨平台工具包 disallow/tell 的原因。有时它可能会起作用,但你不能说为什么,甚至不能说它会继续起作用。)

使用 Vulkan 和 DirectX 12(我认为但不确定 Metal),您可以从多个线程进行渲染。哇哦!当然,现在你必须弄清楚如何在不使整个事情比单线程慢的情况下进行所有协调、锁定和交叉同步,但至少你可以选择尝试。

除了 Matt 的出色回答外,您还可以使用 Qt 程序使用 invokeMethodpostEvent 让后台线程安全地更新 UI。