检测是不是在 WPF 和 Winforms 中的 UI 线程上

Posted

技术标签:

【中文标题】检测是不是在 WPF 和 Winforms 中的 UI 线程上【英文标题】:Detecting whether on UI thread in WPF and Winforms检测是否在 WPF 和 Winforms 中的 UI 线程上 【发布时间】:2011-07-05 20:12:43 【问题描述】:

我在下面编写了一个断言方法Ensure.CurrentlyOnUiThread(),用于检查当前线程是否为 UI 线程。

这在检测 Winforms UI 线程时是否可靠? 我们的应用混合了 WPF 和 Winforms,如何最好地检测有效的 WPF UI 线程? 有没有更好的方法来做到这一点?也许是代码合同?

Ensure.cs

using System.Diagnostics;
using System.Windows.Forms;

public static class Ensure

    [Conditional("DEBUG")]
    public static void CurrentlyOnUiThread()
    
        if (!Application.MessageLoop)
        
            throw new ThreadStateException("Assertion failed: not on the UI thread");
        
    

【问题讨论】:

@chilliton 为什么你需要知道你是否在 UIThread 上? 它旨在作为一个断言,以确保允许修改 UI 的代码这样做。 所有答案对您有帮助吗?如果是这样,最好接受一个。 【参考方案1】:

不要使用

if(Dispatcher.CurrentDispatcher.Thread == Thread.CurrentThread)

   // Do something

如果当前线程没有调度程序,Dispatcher.CurrentDispatcher 将创建并返回与当前线程关联的新Dispatcher

改为这样做

Dispatcher dispatcher = Dispatcher.FromThread(Thread.CurrentThread);
if (dispatcher != null)

   // We know the thread have a dispatcher that we can use.

为了确保您有正确的调度程序或在正确的线程上,您有以下选项

Dispatcher _myDispatcher;

public void UnknownThreadCalling()

    if (_myDispatcher.CheckAccess())
    
        // Calling thread is associated with the Dispatcher
    

    try
    
        _myDispatcher.VerifyAccess();

        // Calling thread is associated with the Dispatcher
    
    catch (InvalidOperationException)
    
        // Thread can't use dispatcher
    

CheckAccess()VerifyAccess() 不会出现在智能感知中。

另外,如果你不得不诉诸这些事情,很可能是因为糟糕的设计。您应该知道哪些线程在您的程序中运行哪些代码。

【讨论】:

我已经尝试了提供的解决方案,但得出的结论是它不能从由 Task.Run() 初始化的后台线程中工作。此处提供的解决方案运行良好:***.com/a/13726324/249948. 请注意,Herman Cordes 链接的解决方案适用于 WPF。对于 WinForms,使用 CodeMonkey 的答案,或者干脆做if (System.Windows.Forms.Application.MessageLoop)【参考方案2】:

在你通常会使用的 WinForms 中

if(control.InvokeRequired) 

 // Do non UI thread stuff

用于 WPF

if (!control.Dispatcher.CheckAccess())

  // Do non UI Thread stuff

我可能会编写一个小方法,使用通用约束来确定您应该调用哪些。例如

public static bool CurrentlyOnUiThread<T>(T control)
 
   if(T is System.Windows.Forms.Control)
   
      System.Windows.Forms.Control c = control as System.Windows.Forms.Control;
      return !c.InvokeRequired;
   
   else if(T is System.Windows.Controls.Control)
   
      System.Windows.Controls.Control c = control as System.Windows.Control.Control;
      return c.Dispatcher.CheckAccess()
   

【讨论】:

感谢这些,我发现 InvokeRequired 非常不可靠,尤其是如果控件还没有句柄或正在处置,我也想要一个不需要访问的断言到控件。 如果您的项目正在被处置或尚未正确初始化,您为什么要调用 InvokeRequired?使用 InvokeRequired 是标准的... 你是对的,我不应该,这应该是其他一些检查的主题。 只是检查一下,您确实意识到您可以通过调用从无 UI 线程更新 UI 元素,不是吗? @Ian - 不错,方便的方法。注意:返回类型应为 bool 而不是 void。方法应以else throw new InvalidArgumentException... 结尾【参考方案3】:

对于 WPF,我使用以下内容:

public static void InvokeIfNecessary (Action action)

    if (Thread.CurrentThread == Application.Current.Dispatcher.Thread)
        action ();
    else 
        Application.Current.Dispatcher.Invoke(action);
    

关键是不是检查 Dispatcher.CurrentDispatcher(它将为您提供当前线程的调度程序),您需要检查当前线程是否与应用程序或其他控件的调度程序匹配。

【讨论】:

这是这里唯一可靠的解决方案。投票最多的答案通常会为线程池线程返回一个非空调度程序。 发现当您在后台线程上时,接受的答案有时会返回非空调度程序。这似乎更可靠。 在 UI 线程执行分支中,添加此处建议的 DoEvents ***.com/a/4502200/282694 以强制在 UI 上更新操作结果(只需 " action(); DoEvents(); " 而不是 "动作()") 使用一天后的另一个观察结果。如果在锁或并行处理中使用它 - 这可能会导致 Invoke() 出现死锁。建议的解决方案是用 BeginInvoke() 替换它,但它将是异步执行,在许多情况下只更新 UI 是可以的,但如果在接下来的步骤中使用此执行的结果,则可能不行。【参考方案4】:

对于 WPF:

// You are on WPF UI thread!
if (Thread.CurrentThread == System.Windows.Threading.Dispatcher.CurrentDispatcher.Thread)

对于 WinForms:

// You are NOT on WinForms UI thread for this control!
if (someControlOrWindow.InvokeRequired)

【讨论】:

只有当someControlorWindow创建于希望被检查的 UI 线程时,后一种情况才成立。拥有多个 Windows UI 线程是完全“可以”的。 @pst - 可能没问题,但很少见。在几乎所有情况下,WPF 和 WinForms 应用程序都只有一个 UI 线程。 关于 WPF,这个答案是不正确的。请记住,每个线程都有一个调度程序。所以基本上在你的陈述中你问的是“这个线程是否等于与这个线程的调度程序相关的线程?”或者,再浓缩一点,你在问“这个线程等于这个线程吗?”嗯,是的,当然是。请参阅此处的 MS 文档:msdn.microsoft.com/en-us/library/… WPF 中的每个线程都没有调度程序!但他们可以得到一个!如果当前线程没有 Dispatcher.CurrentDispatcher 将创建并返回一个新的 Dispatcher!!! @Goran - 你说的是真的,但你误解了山姆的观点。对 CurrentDispatcher 的调用将永远不会返回与 Thread.CurrentThread 不同的线程的调度程序。相反,如果 CurrentThread 缺少一个调度程序,它将为其创建一个调度程序——并且该调度程序的线程将是 CurrentThread。所以测试总是评估为CurrentThread == CurrentThread,所以true。所以这个答案是错误的。【参考方案5】:

也许 Control.InvokeRequired (WinForms) 和 Dispatcher.CheckAccess (WPF) 适合您?

【讨论】:

【参考方案6】:

您正在将您的 UI 知识融入到您的逻辑中。这不是一个好的设计。

您的 UI 层应该处理线程,因为确保 UI 线程不被滥用是在 UI 的范围内。

这还允许您在 winforms 中使用 IsInvokeRequired,在 WPF 中使用 Dispatcher.Invoke...并允许您在同步和异步 asp.net 请求中使用您的代码...

我在实践中发现,尝试在应用程序逻辑中处理较低级别的线程通常会增加许多不必要的复杂性。事实上,实际上整个框架都是在承认这一点的情况下编写的——框架中几乎没有任何东西是线程安全的。由调用者(更高级别)来确保线程安全。

【讨论】:

当然,但这不是设计的一部分,它是我可以采取的一系列检查和措施的一部分,以确保系统的其他部分正常/按预期运行。在处理意大利面条遗留代码时,我发现这类断言非常有用。 @chillitom 我的哀悼。我想如果我处于你的位置,我会很想掷骰子,快速失败,并了解意大利面条的哪些股线容易受到线程问题的影响,并尽可能将它们隔离。【参考方案7】:

这是我在 WPF 中使用的一段代码,用于捕获从非 UI 线程修改 UI 属性(实现 INotifyPropertyChanged)的尝试:

    public event PropertyChangedEventHandler PropertyChanged;

    private void NotifyPropertyChanged(String info)
    
        // Uncomment this to catch attempts to modify UI properties from a non-UI thread
        //bool oopsie = false;
        //if (Thread.CurrentThread != Application.Current.Dispatcher.Thread)
        //
        //    oopsie = true; // place to set a breakpt
        //

        if (PropertyChanged != null)
        
            PropertyChanged(this, new PropertyChangedEventArgs(info));
        
    

【讨论】:

【参考方案8】:

对于 WPF:

我需要知道我线程上的 Dispatcher 是否实际启动。因为如果您在线程上创建任何 WPF 类,接受的答案将声明调度程序在那里,即使您从未执行过Dispatcher.Run()。我最终得到了一些反思:

public static class WpfDispatcherUtils

    private static readonly Type dispatcherType = typeof(Dispatcher);
    private static readonly FieldInfo frameDepthField = dispatcherType.GetField("_frameDepth", BindingFlags.Instance | BindingFlags.NonPublic);

    public static bool IsInsideDispatcher()
    
        // get dispatcher for current thread
        Dispatcher currentThreadDispatcher = Dispatcher.FromThread(Thread.CurrentThread);

        if (currentThreadDispatcher == null)
        
            // no dispatcher for current thread, we're definitely outside
            return false;
        

        // get current dispatcher frame depth
        int currentFrameDepth = (int) frameDepthField.GetValue(currentThreadDispatcher);

        return currentFrameDepth != 0;
    

【讨论】:

【参考方案9】:

您可以像这样比较线程 ID:

       var managedThreadId = System.Windows.Threading.Dispatcher.FromThread(System.Threading.Thread.CurrentThread)?.Thread.ManagedThreadId;
        var dispatcherManagedThreadId = System.Windows.Application.Current.Dispatcher.Thread.ManagedThreadId;
        if (managedThreadId == dispatcherManagedThreadId)
        
             //works in ui dispatcher thread
        

【讨论】:

【参考方案10】:

使用 MVVM 实际上相当容易。我所做的就是在 ViewModelBase 中放入类似以下内容...

protected readonly SynchronizationContext SyncContext = SynchronizationContext.Current;

或者...

protected readonly TaskScheduler Scheduler = TaskScheduler.Current; 

然后,当特定的 ViewModel 需要触摸任何“可观察”的东西时,您可以检查上下文并做出相应的反应......

public void RefreshData(object state = null /* for direct calls */)

    if (SyncContext != SynchronizationContext.Current)
    
        SyncContext.Post(RefreshData, null); // SendOrPostCallback
        return;
    
    // ...

或在返回上下文之前在后台执行其他操作...

public void RefreshData()

    Task<MyData>.Factory.StartNew(() => GetData())
        .ContinueWith(t => /* Do something with t.Result */, Scheduler);

通常,如果您以有序的方式遵循 MVVM(或任何其他架构),则很容易判断 UI 同步的责任在哪里。但是您基本上可以在任何地方执行此操作以返回创建对象的上下文。我敢肯定,在一个庞大而复杂的系统中,创建一个“守卫”来干净、一致地处理这个问题是很容易的。

我认为说你唯一的责任是回到你自己的原始环境是有道理的。客户也有责任这样做。

【讨论】:

读者应该知道这种方法在 .NET 4 上并不可靠。有一个 bug 会导致 UI 线程上的 SynchronizationContext.Current 为空。见***.com/questions/4659257。【参考方案11】:

对于 WPF:

这是基于最佳答案的 sn-p,使用委托意味着它非常通用。

        /// <summary>
        /// Invokes the Delegate directly on the main UI thread, based on the calling threads' <see cref="Dispatcher"/>.
        /// NOTE this is a blocking call.
        /// </summary>
        /// <param name="method">Method to invoke on the Main ui thread</param>
        /// <param name="args">Argumens to pass to the method</param>
        /// <returns>The return object of the called object, which can be null.</returns>
        private object InvokeForUiIfNeeded(Delegate method, params object[] args)
        
            if (method == null) throw new ArgumentNullException(nameof(method));

            var dispatcher = Application.Current.Dispatcher;

            if (dispatcher.Thread != Thread.CurrentThread)
            
                // We're on some other thread, Invoke it directly on the main ui thread.
                return dispatcher.Invoke(method, args);
            
            else
            
                // We're on the dispatchers' thread, which (in wpf) is the main UI thread.
                // We can safely update ui here, and not going through the dispatcher which safes some (minor) overhead.
                return method.DynamicInvoke(args);
            

        

        /// <inheritdoc cref="InvokeForUiIfNeeded(Delegate, object[])"/>
        public TReturn InvokeForUiIfNeeded<TReturn>(Delegate method, params object[] args)
            => (TReturn) InvokeForUiIfNeeded(method, args);

第二种方法允许更安全的返回类型。 我还添加了一些重载,它们会在我的代码中自动采用 FuncAction 参数,例如:

        /// <inheritdoc cref="InvokeForUiIfNeeded(System.Delegate, object[])"/>
        private void InvokeForUiIfNeeded(Action action)
            => InvokeForUiIfNeeded((Delegate) action);

注意; FuncAction 继承自 Delegate,所以我们可以直接转换它。

您也可以添加自己的通用重载来执行操作,我没有费心创建一堆重载,但您绝对可以,例如;

        /// <inheritdoc cref="InvokeForUiIfNeeded(System.Delegate, object[])"/>
        private void InvokeForUiIfNeeded<T1>(Action<T1> action, T1 p1)
            => InvokeForUiIfNeeded((Delegate)action, p1);

        /// <inheritdoc cref="InvokeForUiIfNeeded(System.Delegate, object[])"/>
        private TReturn InvokeForUiIfNeeded<T1, TReturn>(Func<T1, TReturn> action, T1 p1)
            => (TReturn)InvokeForUiIfNeeded((Delegate)action, p1);

【讨论】:

【参考方案12】:
Thread.CurrentThread.ManagedThreadId == Dispatcher.Thread.ManagedThreadId

这是一种更好的检查方法

【讨论】:

以上是关于检测是不是在 WPF 和 Winforms 中的 UI 线程上的主要内容,如果未能解决你的问题,请参考以下文章

在 WPF 中检测 Drag'n'Drop 文件?

WPF 有 e.CloseReason 吗?

MSDN 中的“应用程序”指的是啥?使用 .NET 本机编译 Winforms 和 WPF

WinForms中的WPF控件[关闭]

在 VS 2010 的 Winforms 项目中添加 WPF 窗口

如何编写自动扩展到系统字体和dpi设置的WinForms代码?