在GUI编程中,调用方为什么要确保线程安全?


37

我已经在许多地方看到,规范知识1是调用者的责任,以确保您在更新UI组件时位于UI线程上(特别是在Java Swing中,您位于Event Dispatch Thread上) 。

为什么会这样呢?事件分发线程是MVC / MVP / MVVM中视图的关注点;在视图之外的任何地方处理它,都会在视图的实现和该视图的实现的线程模型之间建立紧密的耦合

具体来说,假设我有一个使用Swing的MVC架构的应用程序。如果调用者负责更新事件调度线程上的组​​件,则如果我尝试将Swing View实现换成JavaFX实现,则必须更改所有Presenter / Controller代码以改为使用JavaFX Application线程

因此,我想我有两个问题:

  1. 为什么调用者有责任确保UI组件线程安全?我上面的推理中的缺陷在哪里?
  2. 我该如何设计我的应用程序以松散耦合这些线程安全问题,但仍然是适当的线程安全?

让我添加一些MCVE Java代码来说明“调用者负责”的含义(这里还没有其他好的做法,但我试图将其尽量减少):

来电者负责:

public class Presenter {
  private final View;

  void updateViewWithNewData(final Data data) {
    EventQueue.invokeLater(new Runnable() {
      public void run() {
        view.setData(data);
      }
    });
  }
}
public class View {
  void setData(Data data) {
    component.setText(data.getMessage());
  }
}

查看负责人:

public class Presenter {
  private final View;

  void updateViewWithNewData(final Data data) {
    view.setData(data);
  }
}
public class View {
  void setData(Data data) {
    EventQueue.invokeLater(new Runnable() {
      public void run() {
        component.setText(data.getMessage());
      }
    });
  }
}

1:该文章的作者在“ Swing on Stack Overflow”中得分最高。他在所有地方都这么说,而且我也认为这也是其他地方呼叫者的责任。


1
恩,表演恕我直言。这些事件发布不是免费提供的,在非平凡的应用程序中,您希望最小化它们的数量(并确保没有一个太大),但是从逻辑上讲,最小化/压缩应该在演示者中进行。
2015年

1
@Ordous在将线程切换放入视图中时,您仍然可以确保发布的内容最少。
durron597

2
前段时间,我读了一个非常不错的博客,它讨论了这个问题,它的基本含义是尝试使UI工具包线程安全是非常危险的,因为它可能引入死锁,并且根据实现方式的不同,还会出现竞争条件。框架。还有性能方面的考虑。现在还不算很多,但是当Swing首次发布时,它的性能受到很大的批评(变差了),但这并不是Swing的错,这是人们缺乏如何使用它的知识。
MadProgrammer

1
SWT通过抛出异常(如果不是很漂亮)引发异常来实施线程安全的概念,但至少您已经意识到了这一点。您提到从Swing更改为JavaFX,但是几乎所有UI框架都会遇到此问题,Swing似乎是突出此问题的一个。您可以设计一个中间层(控制器的控制器?),以确保对UI的调用正确同步。从UI API的角度确切地知道如何设计API的非UI部分是不可能的
MadProgrammer 2015年

1
而且大多数开发人员会抱怨,UI API中实施的任何线程保护都是限制性的或不满足其需求。最好让您根据自己的需求来决定如何解决此问题
MadProgrammer

Answers:


22

在他失败的梦想论文的结尾,Graham Hamilton(一位主要的Java架构师)提到,如果开发人员“要使用事件队列模型来保持等价性,他们将需要遵循各种不明显的规则”,并具有可见的和显式的事件队列模型“似乎可以帮助人们更可靠地遵循该模型,从而构建可靠运行的GUI程序。”

换句话说,如果您尝试将多线程外观放置在事件队列模型之上,则抽象有时会以非显而易见的方式泄漏,这非常难以调试。似乎可以在纸上使用,但最终在生产中分崩离析。

在单个组件周围添加小型包装器可能不会有问题,例如从工作线程更新进度条。如果您尝试执行需要多个锁的更复杂的操作,那么就很难开始推断多线程层和事件队列层如何相互作用。

请注意,这些问题对于所有GUI工具包都是通用的。假设您的演示者/控制器中的事件分发模型不是将您紧密地耦合到一个特定的GUI工具箱的并发模型,而是将您耦合到所有这些工具。事件排队接口应该不那么难抽象。


25

因为使GUI lib线程安全是一个巨大的麻烦和瓶颈。

GUI中的控制流通常从事件队列到根窗口再到gui小部件,从应用程序代码到传播到根窗口的小部件有两个方向。

制定锁定策略未锁根窗口(会引起很多争论的)是困难的。在另一个线程从上向下锁定时将底部向上锁定是实现即时死锁的好方法。

并且每次都检查当前线程是否为gui线程是昂贵的,并且可能导致对gui实际发生的混乱,尤其是在执行读取更新写入序列时。这需要锁定数据以避免竞争。


1
有趣的是,我通过为我编写的用于管理线程切换的这些更新提供一个排队框架来解决此问题。
durron597

2
@ durron597:而且您永远不会有任何更新取决于其他线程可能会影响的UI的当前状态?然后它可能会起作用。
Deduplicator 2015年

为什么需要嵌套锁?在子窗口中处理细节时,为什么需要锁定整个根窗口?锁顺序是一个可解决的问题,它提供了一种公开的方法来以正确的顺序(自上而下或自下而上,但不要把选择权留给调用者)来锁定多重锁
MSalters

1
@MSalters为root我是指当前窗口。要获得所有需要获得的锁,您需要走上层次结构,这需要在遇到每个容器时锁定每个容器,以获取父容器并进行解锁(以确保您仅自上而下锁定),然后希望它没有改变在获得根窗口之后,您将自上而下进行锁定。
棘手怪胎

@ratchetfreak:如果您尝试锁定的孩子在锁定时被另一个线程删除,那只是有点不幸,但与锁定无关。您只是不能对刚刚删除了另一个线程的对象进行操作。但是,为什么其他线程删除了您的线程仍在使用的对象/窗口?这在任何情况下都不好,而不仅仅是UI。
MSalters 2015年

17

线程性(在共享内存模型中)是一种倾向于违背抽象工作的属性。一个简单的示例是Set-type:while Contains(..)Add(...)and Update(...)在单线程方案中是完全有效的API,而多线程方案则需要一个AddOrUpdate

UI上也有同样的事情-如果要显示项目列表,并在列表顶部显示项目数,则每次更改都需要更新两者。

  1. 该工具箱无法解决该问题,因为锁定不能确保操作顺序正确无误。
  2. 该视图可以解决问题,但前提是您允许以下业务规则:列表顶部的数字必须与列表中的项目数匹配,并且仅通过视图更新列表。不完全应该是MVC。
  3. 演示者可以解决它,但需要意识到该视图在线程方面有特殊需求。
  4. 数据绑定到支持多线程的模型是另一种选择。但这会使模型复杂化,而这应该是UI关注的问题。

这些看起来都不是很诱人。建议不要让主持人负责处理线程,这不是因为这样做很好,而是因为它可以工作并且替代方法更糟。


也许可以在视图和演示者之间引入一个层,也可以将其换出。
durron597

2
.NET System.Windows.Threading.Dispatcher可以处理向WPF和WinForms UI-Threads的调度。演示者和视图之间的这种层绝对有用。但是它仅提供工具包独立性,而不提供线程独立性。
Patrick

9

前段时间,我读了一个非常好的博客,讨论了这个问题(卡尔·比勒费尔特提到),它的基本含义是尝试使UI工具包线程安全是非常危险的,因为它会引入可能的死锁,并取决于死锁的方式。实施后,将竞赛条件纳入框架。

还有性能方面的考虑。现在还不算很多,但是当Swing首次发布时,它的性能受到很大的批评(变差了),但这并不是Swing的错,这是人们缺乏如何使用它的知识。

SWT通过抛出异常(如果不是很漂亮)引发异常来实施线程安全的概念,但至少您已经意识到了这一点。

例如,如果您查看绘画过程,则绘制元素的顺序非常重要。您不希望一个组件的绘制在屏幕的任何其他部分产生副作用。想象一下,如果您可以更新标签的text属性,但是它是由两个不同的线程绘制的,则可能会导致输出损坏。因此,所有绘制通常在一个线程中完成,通常基于需求/请求的顺序(但有时会缩短以减少实际的物理绘制周期数)

您提到从Swing更改为JavaFX,但是几乎所有UI框架(不仅是胖客户端,而且还有Web)都会遇到此问题,Swing似乎是突出问题的一种。

您可以设计一个中间层(控制器的控制器?),以确保对UI的调用正确同步。从UI API的角度确切地知道如何设计API的非UI部分是不可能的,大多数开发人员会抱怨UI API中实现的任何线程保护都是限制性的或不满足其需求。最好让您根据自己的需求来决定如何解决此问题

您需要考虑的最大问题之一是基于已知输入来证明给定事件顺序合理的能力。例如,如果用户调整窗口大小,那么事件队列模型将保证将发生给定事件顺序,这似乎很简单,但是如果队列允许事件被其他线程触发,那么您将无法再保证该顺序。哪些事件可能发生(竞赛条件),突然之间,您必须开始担心不同的状态,并且在其他事情发生之前不做任何事情,并且开始必须共享周围的状态标志,最终您会得到意大利面条。

好的,您可以通过安排某种队列来解决此问题,该队列根据事件的发布时间对其进行排序,但这不是我们已经拥有的吗?此外,您仍然不能保证线程B在线程A之后会生成它的事件

人们对于不得不考虑自己的代码而感到不高兴的主要原因是,他们不得不考虑自己的代码/设计。“为什么不能更简单?” 它不可能简单,因为这不是一个简单的问题。

我记得当PS3发行时,索尼正在谈论Cell处理器,它具有执行单独的逻辑行,解码音频,视频,加载和解析模型数据的能力。一位游戏开发人员问:“那太棒了,但是如何同步流?”

开发人员正在谈论的问题是,在某些时候,所有这些单独的流都需要向下同步到单个管道以进行输出。可怜的主持人只是耸了耸肩,因为这不是他们所熟悉的问题。显然,他们现在有解决此问题的解决方案,但当时很有趣。

现代计算机正在同时从许多不同的地方获取大量输入,所有这些输入都需要进行处理并传递给用户,这不会干扰其他信息的显示,因此这是一个复杂的问题,一个简单的解决方案。

现在,具有切换框架的能力,这并不是一件容易的事,但是,花一点时间使用MVC,MVC可以是多层的,也就是说,您可以拥有直接处理UI框架的MVC,您可以然后可以将其包装在高层MVC中,该高层MVC处理与其他(可能是多线程)框架的交互,这是确定较低MVC层如何进行通知/更新的责任。

然后,您将使用编码来接口设计模式,并使用工厂或构建器模式来构造这些不同的层。这意味着,多线程框架通过使用中间层与UI层分离,这是一个想法。


2
您不会在网络上遇到此问题。JavaScript故意不提供线程支持-JavaScript运行时实际上只是一个大事件队列。(是的,我知道严格来讲JS具有WebWorkers-它们是受精疲力的线程,其行为更像其他语言中的actor)。
James_pic

1
@James_pic您实际拥有的是浏览器中充当事件队列同步器的部分,这基本上就是我们一直在谈论的内容,调用方负责确保工具箱事件队列发生更新
MadProgrammer

对,就是这样。在Web上下文中,主要区别在于,无论调用者的代码如何,都会发生这种同步,因为运行时没有提供允许事件队列外的代码执行的机制。因此,呼叫者无需对此负责。我相信,这也是开发NodeJS的主要动机之一-如果运行时环境是事件循环,那么默认情况下所有代码都是事件循环感知的。
James_pic

1
就是说,有些浏览器UI框架具有自己的事件循环(在事件循环内)(我在看Angular)。因为这些框架不再受运行时的保护,所以调用者还必须确保在事件循环内执行代码,类似于其他框架中的多线程代码。
James_pic

使用“仅显示”控件会自动提供线程安全是否会有任何特定问题?如果一个控件具有一个可编辑的文本控件,而该控件恰好由一个线程编写并由另一个线程读取,则没有一种好方法可以使该写操作对线程可见,而又不将这些操作中的至少一个与控件的实际UI状态同步,但是对于仅显示控件有什么问题吗?我认为这些功能可以轻松地为在其上执行的操作提供所需的锁定或互锁,并允许调用方忽略何时进行计时……
supercat
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.