一个方法如何知道它是否在UI线程上运行?

3

我有一个简单的问题,但我大约80%确定问题的答案将伴随着“你做错了”的回答,所以我也会问个不简单的问题。

简单问题:我有一个公共类的公共方法。如果在UI线程上调用它,则希望它抛出异常。我该怎么做?

更不简单的问题是:是否有更容易重构这个设计的方法?

背景:

我开发了一个桌面程序,通过其API与遗留应用进行交互。该API不支持远程线程安全。我建立了一个类库,尽可能地封装与API的交互(这涉及到在巨大的byte []缓冲区中编组和解组数据,然后调用从DLL加载的外部函数)。因为我知道创建多个核心API对象会是一场灾难,所以我将其实现为静态类。

我还构建了一组在我的应用程序后台运行任务的类。 TaskManager维护Task对象队列,并使用BackgroundWorker运行它们。使用后台线程可以在进行与混乱的旧版应用程序的交互时保持桌面应用程序的UI响应性;使用队列确保在任何给定时间只有一个任务正在调用API。

不幸的是,我从没想过在这个设计中建立某些保障措施。最近我发现了代码中直接在UI线程上调用API的地方。我相信我已经修复了它们,但我想保证我不会再犯同样的错误。

如果我从一开始就正确地设计了这个东西,我将API包装器类设置为非静态类,使其构造函数对除TaskManager之外的所有内容进行隐藏,并将API类的实例传递给创建每个Task时。任何Task调用的方法都需要传递API对象。这将使在前台线程上使用API变得不可能。

问题是,有很多代码与API交互。实施此更改(我认为最终是正确的事情)将触及所有内容。所以在此期间,我想修改API的Call方法,以便在前台线程上调用时抛出异常。

我知道我正在解决错误的问题。我可以感受到它在我的骨头里。但我现在也被它缠住了,无法找到正确的解决方案。

编辑:

我之前提出的问题表述不清,所以很难回答。我的问题不应该是“这个方法如何知道它是否在UI线程上运行?”真正的问题是:“这个方法如何知道它是否在错误的线程上运行?”理论上可能有一千个线程在运行。正如JaredPar所指出的那样,可能会有多个UI线程。只有一个线程是正确的线程,而且它的线程ID很容易找到。

事实上,即使我重构这段代码,使其设计得更好(今天我大部分时间都在做这件事),也值得在API中添加机制来检查它是否在适当的线程上运行。

3个回答

3

确定当前是否在UI线程的部分问题是可能存在多个UI线程。在WPF和WinForms中,可以创建多个用于显示UI的线程。

但在这种情况下,听起来您的情况相对较为受限。最好的方法是记录UI线程或后台线程的ID,并使用Thread.CurrentThread.ManagedThreadId确保您在正确的线程上。

public class ThreadUtil {
  public static int UIThreadId;

  public static void EnsureNotUIThread() {
    if ( Thread.CurrentThread.ManagedThreadId == UIThreadId ) {
      throw new InvalidOperationException("Bad thread");
    }
  }
}

这种方法有一些注意事项。您必须以原子方式设置UIThreadId,并且必须在任何后台代码运行之前这样做。最好的方法可能是将以下行添加到程序启动中。

Interlocked.Exchange(ref ThreadUtil.UIThreadID, Thread.CurrentThread.ManagedThreadId);

另一个技巧是查找 SynchronizationContext。WinForms 和 WPF 在它们的 UI 线程上设置了 SynchronizationContext,以便允许与后台线程通信。对于由您的程序创建和控制的后台线程(我真的想强调这一点),除非您实际安装一个,否则不会安装 SynchronizationContext。所以以下代码可以在那种非常有限的情况下使用:

public static bool IsBackground() { 
  return null == SynchronizationContext.Current;
}

1
这与我实际所做的非常接近。我所做的是在调用API的任务类上设置一个ThreadID属性,然后再进行调用。每个API调用都会抛出一个异常,如果当前线程的ID不等于此ThreadID,则会抛出异常。虽然UI开发人员可以绕过此限制,但由于异常消息明确指出“不要从UI线程调用API”,因此我认为如果他遇到这种情况,他不会这样做。另外,当我启用此功能时,我在UI代码中找到了半打API调用。所以:胜利。 - Robert Rossney

1

我会反转ISynchronizeInvoke的使用情况,并在ISynchronizeinvoke.InvokeRequired == false时抛出异常。这样WinForms就可以处理找到UI线程的繁琐工作了。性能会有点差,但这是一个调试场景 - 所以并不重要。你甚至可以将检查隐藏在#IF DEBUG标志后面,只在调试版本上进行检查。

你确实需要给你的API一个ISynchronizeInvoke的引用 - 但你可以在启动时轻松地做到这一点(只需传递你的主窗体),或者让它使用静态的Form.ActiveForm调用。


0

Justin Rogers详细讨论了Invoke/BeginInvoke的内部机制,以及可能出现的问题。

基本上,它需要在UI线程中查找窗口,并调用GetWindowThreadProcessId比较当前线程ID和窗口ID。(当然,如果只有一个UI线程,可以缓存线程ID)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接