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

53

我编写了一个断言方法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 上? - msarchet
8
这句话的意思是:它旨在确保修改用户界面的代码有权这样做。 - chillitom
1
有没有任何答案对您有所帮助?如果有的话,接受其中一个会很好。 - Ian
12个回答

68
不要使用。
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()在Intellisense中不显示。

此外,如果你必须采取这种方式,那很可能是因为设计不良。你应该知道哪些线程在程序中运行什么代码。


6
我已经尝试了提供的解决方案,但是得出结论它在由Task.Run()初始化的后台线程中不起作用。这里提供的解决方案有效:https://dev59.com/4nA85IYBdhLWcg3wBOtO#13726324。 - Herman Cordes
2
请注意,Herman Cordes 提供的解决方案适用于 WPF。对于 WinForms,请使用 CodeMonkey 的答案,或者简单地执行 if (System.Windows.Forms.Application.MessageLoop) - ToolmakerSteve

28

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

public static void InvokeIfNecessary (Action action)
{
    if (Thread.CurrentThread == Application.Current.Dispatcher.Thread)
        action ();
    else {
        Application.Current.Dispatcher.Invoke(action);
    }
}

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


2
这是唯一可靠的解决方案。最受欢迎的答案经常为线程池线程返回非空调度程序。 - Kirk Woll
发现当在后台线程时,有时候被接受的答案会返回非空的dispatcher。这似乎更加可靠。 - Fred
在UI线程执行分支中,可能还可以添加DoEvents,建议在这里https://dev59.com/yG855IYBdhLWcg3wKw5Y#4502200中强制更新UI上的操作结果(只需“{ action(); DoEvents(); }”而不是“action()”)。 - sarh
1
使用一天后的另一个观察结果是,如果它在锁定或并行处理内使用,这可能会导致Invoke()死锁。建议解决方案是将其替换为BeginInvoke(),但这将是异步执行,在许多情况下仅用于更新UI,但如果此执行的结果在下一步中使用,则可能不适用。 - sarh

25

在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()
   }
}

2
谢谢,我发现InvokeRequired非常不可靠,特别是如果控件还没有句柄或正在被释放,此外,我想要一个不需要访问控件的断言。 - chillitom
1
如果您的项正在被处理或尚未正确初始化,为什么要调用InvokeRequired?使用InvokeRequired是标准做法... - Ian
你是对的,我不应该这样做,这应该成为其他检查的主题。 - chillitom
只是为了确认,您是否意识到可以通过调用从非 UI 线程更新 UI 元素呢? - Ian
@Ian,是的,我明白了。这更像是一种尝试,在程序到达代码中假定已经在GUI线程上但实际上在错误线程上时,将一些逻辑放入以快速失败。并非每个方法都可以访问控件来执行检查。因此有这个问题。 - chillitom
1
@Ian - 不错,很方便的方法。注意:返回类型应该是 bool 而不是 void。并且方法应该以 else throw new InvalidArgumentException... 结尾。 - ToolmakerSteve

18

对于 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)

3
仅当someControlorWindow是在希望进行检查的UI线程上创建时,后一种情况才成立。拥有多个Windows UI线程是完全可以的。 - user166390
3
可能可以,但这种情况很少见。几乎所有情况下,WPF和WinForms应用程序都只有一个UI线程。 - Bevan
9
关于WPF,这个回答是不正确的。请记住,每个线程都有一个调度程序。因此,在您的陈述中,您正在问“该线程是否等于该线程的调度程序关联的线程?”或者更为简洁地说,“该线程是否等于此线程?”嗯,当然是的。请参阅Microsoft文档:http://msdn.microsoft.com/en-us/library/system.windows.threading.dispatcher.checkaccess.aspx#feedback - Sam
4
WPF 中的每个线程并不都有一个调度程序(dispatcher)!但是它们可以获取一个调度程序。 如果当前线程没有调度程序,Dispatcher.CurrentDispatcher 将创建并返回一个新的调度程序! - CodeMonkey
5
@Goran - 你所说的是正确的,但你误解了Sam的观点。调用CurrentDispatcher永远不会返回属于Thread.CurrentThread不同线程的调度程序。相反,如果CurrentThread缺少调度程序,它将为其创建一个调度程序,并且该调度程序的线程将是CurrentThread。因此,测试将始终评估为“CurrentThread == CurrentThread”,因此为“true”。因此,这个答案是错误的。 - ToolmakerSteve
显示剩余2条评论

6
也许Control.InvokeRequired(WinForms)和Dispatcher.CheckAccess(WPF)对您来说是可以的?

2
你将UI的知识推入了逻辑中,这不是一个好的设计。
你的UI层应该处理线程,因为确保不滥用UI线程是UI的职责范围。
这也使你可以在winforms中使用IsInvokeRequired,在WPF中使用Dispatcher.Invoke,并且还允许你在同步和异步asp.net请求中使用你的代码...
实际上,我发现在应用程序逻辑的较低级别处理线程通常会增加很多不必要的复杂性。事实上,几乎整个框架都是按照这一点来编写的 - 几乎没有什么是线程安全的。这取决于调用者(在更高级别)来确保线程安全。

1
当然可以,但这不是设计的一部分,而是我可以采取的一系列检查和措施的一部分,以确保系统的其他部分行为正确/符合预期。在处理意大利面式的遗留代码时,我发现这些断言非常有用。 - chillitom
@chillitom 我向你表示慰问。如果我处在你的位置,我会尝试冒险,快速失败,并学习哪些意大利面条线程容易出现问题,并在可能的最高层面上进行隔离。 - user1228

1

对于WPF:

我需要知道我的线程上的Dispatcher是否已经启动。因为如果你在该线程上创建任何WPF类,即使您从未执行 Dispatcher.Run(),接受的答案也将说明Dispatcher已经存在。我最终使用了一些反射技巧:

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;
    }
}

1

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

    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));
        }
    }

1
您可以这样比较线程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
        }

0

使用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同步的责任将位于哪里。但是,您基本上可以在任何地方执行此操作,以返回创建对象的上下文。我相信,在大型和复杂的系统中,轻松且一致地处理这个问题,可以创建一个“Guard”。

我认为,可以说您唯一的责任是回到自己最初的上下文。客户端也有同样的责任。


读者应该知道,这种方法在.NET 4上不可靠。存在一个错误,可能会使UI线程的SynchronizationContext.Current为空。请参见https://dev59.com/bW445IYBdhLWcg3w3N6z。 - Wallace Kelly

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