C# 方法如何知道它是否';在UI线程上运行什么?

C# 方法如何知道它是否';在UI线程上运行什么?,c#,.net,multithreading,C#,.net,Multithreading,我有一个简单的问题,但我大约80%肯定这个问题的答案会伴随着“你做错了”,所以我也要问一个不简单的问题 简单的问题是:我有一个公共类的公共方法。我希望它在UI线程上调用时抛出异常。我该怎么做 不那么简单的问题是:有没有更简单的方法来重构这个设计 背景: 我开发了一个桌面程序,可以通过API与遗留应用程序进行互操作。API不是远程线程安全的。我已经构建了一个类库,它封装了与API的互操作(这涉及到在一个巨大的byte[]缓冲区中编组和反编组数据,然后调用从DLL加载的外部函数),以便尽可能多地隐藏

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

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

不那么简单的问题是:有没有更简单的方法来重构这个设计

背景:

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

我还构建了一组类,用于在我的应用程序的后台运行任务。
TaskManager
维护一个
Task
对象队列,并使用
BackgroundWorker
运行它们。使用后台线程允许桌面应用程序的UI在与臃肿的遗留应用程序进行互操作时保持响应;使用队列可以确保在任何给定时间只有一个任务调用API

不幸的是,我从未想过要在这个设计中加入某些安全措施。我最近在代码中发现了我在UI线程上直接调用API的地方。我相信我已经把它们都修好了,但我想保证我不会再这样做了

如果我从一开始就正确地设计了它,我会使API包装类成为非静态的,将其构造函数隐藏在除
TaskManager
之外的所有内容中,然后在创建时将API类的实例传递给每个
任务。
Task
调用的任何与API对话的方法都需要传递给API对象。这将导致无法在前台线程上使用API

问题是,有很多代码与API对话。实施这一改变(我认为这最终是正确的做法)将触及所有方面。因此,在此期间,我想修改API的
调用
方法,以便在前台线程上调用时抛出异常

public class ThreadUtil {
  public static int UIThreadId;

  public static void EnsureNotUIThread() {
    if ( Thread.CurrentThread.ManagedThreadId == UIThreadId ) {
      throw new InvalidOperationException("Bad thread");
    }
  }
}
我知道我解决错了问题。我能感觉到它在我的骨头里。但我现在也非常专注于此,无法找到正确的解决方案

编辑:

我显然把这个问题框错了,这就是为什么很难回答的原因。我不应该问“这个方法如何知道它是否在UI线程上运行?”真正的问题是:“这个方法如何知道它是否在错误的线程上运行?”理论上可能有一千个线程在运行。正如JaredPar指出的,可能有不止一个UI线程。只有一个线程是正确的线程,它的线程ID很容易找到

事实上,即使我重构了这段代码,使其设计合理(我今天主要是这样做的),在API中设置一个机制来检查它是否在适当的线程上运行也是值得的。

详细讨论了Invoke/BeginInvoke是如何做到这一点的,以及它在哪里变得丑陋


基本上,它会退回到在UI线程上查找某个窗口,并调用以将当前线程id与窗口的id进行比较。(当然,当您只有一个UI线程时,您可以缓存线程ID)。

确定您是否在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,否则不会安装SynchronizationContext。所以下面的代码可以在非常有限的情况下使用

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

我将反转ISynchronizeInvoke用例,并在
ISynchronizeInvoke.invokererequired==false时抛出异常。这让WinForms可以处理查找UI线程的繁琐工作。性能会有点糟糕,但这是一个调试场景-所以它真的不重要。您甚至可以将检查隐藏在
#IF DEBUG
标志后面,以仅检查调试版本


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

这和我实际做的非常接近。我所做的是在调用API之前,让调用API的任务类在API包装器上设置ThreadID属性。如果当前线程的ID不等于此线程ID,则每个API调用都会引发异常。如果用户界面开发人员真的愿意,他可以克服这一点,但由于异常消息明确表示“不要从用户界面线程调用API”,我想如果他遇到了这一点,他不会。而且,当我打开这个的时候,我发现了一半-