【问题标题】:Why must/should UI frameworks be single threaded?为什么必须/应该 UI 框架是单线程的?
【发布时间】:2015-10-27 12:15:21
【问题描述】:

之前已经提出过密切相关的问题:

但这些问题的答案在某些方面仍然让我不清楚。

  • 第一个问题的提问者询问多线程是否有助于提高性能,而回答者大多表示不会,因为 GUI 不太可能成为现代硬件上 2D 应用程序的瓶颈.但在我看来,这似乎是一种偷偷摸摸的辩论策略。当然,如果您已经仔细构建了应用程序,除了在 UI 线程上调用 UI 之外什么都不做,您就不会遇到瓶颈。但这可能需要大量工作并使您的代码更加复杂,如果您拥有更快的内核或可以从多个线程进行 UI 调用,那么可能就不值得这样做了。

  • 一个普遍提倡的架构设计是让视图组件没有回调并且不需要锁定任何东西,除了它们的后代。在这样的架构下,你不能让任何线程调用视图对象上的方法,使用每个对象的锁,而不用担心死锁吗?

  • 我对 UI 控件的情况不太有信心,但只要它们的回调仅由系统调用,为什么它们会导致任何 特殊 死锁问题?毕竟,如果回调需要做任何耗时的事情,它们会委托给另一个线程,然后我们又回到了多线程的情况。

  • 如果您可以阻塞 UI 线程,您会从多线程 UI 中获得多少好处?因为异步上的各种新兴抽象实际上让您可以做到这一点。

  • 我所看到的几乎所有讨论都假定将使用手动锁定来处理并发,但人们普遍认为手动锁定在大多数情况下是管理并发的不好方法。当我们考虑到专家建议我们更多使用的并发原语(例如软件事务内存,或避开共享内存以支持消息传递(可能与 go 中的同步))时,讨论将如何变化?

【问题讨论】:

    标签: multithreading swing concurrency erlang locking


    【解决方案1】:

    TL;DR

    这是一种简单的方法,可以强制在最终将按顺序排列的活动中进行排序(屏幕按顺序每秒绘制 X 次)。

    讨论

    在系统中处理具有单一身份的长期持有的资源通常是通过用单个线程、进程、“对象”或其他任何代表与给定语言中的并发性有关的原子单元来表示它们来完成的。回到非空的、疏忽的内核、非分时共享的 One True Thread 时代,这是通过轮询/循环或编写自己的调度系统来手动管理的。在这样的系统中,函数/对象/事物和单一资源之间仍然存在 1::1 映射(或者您在 8 年级之前就发疯了)。

    这与处理网络套接字或任何其他长期资源的方法相同。 GUI 本身只是典型程序管理的众多此类资源之一,并且通常长期存在的资源是事件顺序很重要的地方。

    例如,在聊天程序中,您通常不会编写单个线程。您将拥有一个 GUI 线程、一个网络线程,也许还有其他一些处理日志资源或其他内容的线程。典型系统的速度如此之快并不少见,以至于将日志记录和输入放入进行 GUI 更新的同一线程中更容易,但情况并非总是如此。然而,在所有情况下,通过授予它们一个线程来最容易地推断出每种资源类别,这意味着一个线程用于网络,一个线程用于 GUI,但是许多其他线程对于长期操作或在不阻塞其他资源的情况下管理资源。

    为了让生活更轻松,尽可能地直接在这些线程之间共享数据是很常见的。队列比资源锁更容易推理,并且可以保证排序。大多数 GUI 库要么将要处理的事件排队(因此可以按顺序评估它们),要么立即提交事件所需的数据更改,但在每次重绘循环之前获得对 GUI 状态的锁定。之前发生了什么并不重要,唯一重要的是绘制屏幕时的世界状态当时。这与典型的网络案例略有不同,典型的网络案例需要按顺序发送所有数据并且不能忘记其中的一些数据。

    因此,GUI 框架本身并不是多线程的,它是 GUI 循环需要成为一个线程来合理地管理该单个长期持有的资源。编程示例,通常本质上是微不足道的,通常是单线程的,所有程序逻辑都在与 GUI 循环相同的进程/线程中运行,但这在更复杂的程序中并不常见。

    总结一下

    因为调度难,共享数据管理就更难了,而且单个资源反正只能串行访问,一个线程用来表示每个长期持有的资源和每个长期运行的过程是一种典型的代码结构方式. GUI 只是典型程序将管理的几种资源中的一种。所以“GUI 程序”绝不是单线程的,但 GUI 库通常是。

    在琐碎的程序中,将其他程序逻辑放在 GUI 线程中并没有实现任何惩罚,但是当遇到大量负载或资源管理需要大量阻塞或轮询时,这种方法就会失败,这就是您经常看到的原因几乎所有 GUI 库的尘土飞扬的角落中都提到了事件队列、信号槽消息抽象或其他多线程/处理方法(这里我包括游戏库——而游戏库通常希望你想从本质上构建你的自己的小部件围绕自己的UI概念,基本原理很相似,只是有点低级)。

    [顺便说一句,我最近做了很多 Qt/C++ 和 Wx/Erlang。 The Qt docs do a good job of explaining approaches to multi-threading,GUI 循环的作用,以及 Qt 的信号/槽方法适合抽象的位置(因此您不必考虑并发/锁定/排序/调度/等)。 Erlang 本质上是并发的,但是wx itself is typically started as a single OS process that manages a GUI update loop 和 Erlang 将更新事件作为消息发布给它,并且 GUI 事件作为消息发送到 Erlang 端——因此允许正常的 Erlang 并发编码,但提供单点 GUI 事件排序,以便wx 可以做它的 GUI 更新循环的事情。]

    【讨论】:

    • 谢谢,我明天会查看这些链接。澄清一下,我当然知道 GUI 程序通常是多线程的。这就是为什么单个 UI 线程会限制程序员的部分原因!我的理解是/是限制的主要原因是避免死锁(或其他并发错误)。也许基于消息的异步 API 是正确的方法(但为什么它们没有更受欢迎?)。
    • @gmr 根据我的经验,消息传递和不共享状态的结合是一个巨大的胜利,因为它允许人们将调度与状态问题隔离开来。我认为消息传递不太流行的一个原因是,OOP 的词汇将“消息”与“调用”混为一谈。它们不是同一件事。并发进程/对象之间的消息最好由在其自己的线程中运行的队列来表示。在任何情况下,许多 GUI 应用程序确实在单线程中运行,这仅仅是因为没有/低执行惩罚,而且在 algol 风格的语言中更容易推理。
    • 我今天正在考虑这个问题。消息传递(异步)的问题在于,您永远不知道实际状态是什么,因为它是存储在组件中的状态 + 消息泵中未决状态更改的组合。如果您不共享状态,那么您当然是在复制它。不过,我喜欢该模式的概念简单性。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-04-28
    • 1970-01-01
    • 1970-01-01
    • 2017-11-14
    • 1970-01-01
    • 2011-10-20
    • 1970-01-01
    相关资源
    最近更新 更多