WPF Dispatcher.Invoke“挂起”

22

我有一个比较复杂的WPF应用程序,当尝试使用调度程序在UI线程上调用时,似乎会出现“挂起”或者卡住等情况。

大致流程如下:

  1. 处理按钮的点击事件
  2. 创建一个新线程(STA),该线程会创建一个新的Presenter和UI实例,并调用方法Disconnect
  3. Disconnect方法然后在UI上设置一个名为Name的属性
  4. Name的setter方法使用以下代码设置属性:

    if(this.Dispatcher.Thread != Thread.CurrentThread)
    {
        this.Dispatcher.Invoke(DispatcherPriority.Normal, (ThreadStart)delegate{
            this.Name = value; // Call same setter, but on the UI thread
        });
        return;
    }

    SetValue(nameProperty, value); // I have also tried a member variable and setting the textbox.text property directly.

我的问题是,当调用调度程序的invoke方法时,它似乎每次都挂起,并且调用堆栈指示它在Invoke实现中处于睡眠、等待或加入状态。

那么,我是否做错了什么,可能是明显的或不明显的,或者有更好的方法来跨越到UI线程来设置此属性(以及其他属性)?

编辑: 解决方案是在线程委托结束时(例如执行工作的位置),调用System.Windows.Threading.Dispatcher.Run()。- 感谢所有帮助过我的人。


@Matthew - 实际上,BeginInvoke 没有任何“非最优”的地方;如果你不绝对需要立即更新,那么它是可以的。但是,你需要稍微注意一下捕获的变量(即在调用 BeginInvoke 后不要更改“value”)。 - Marc Gravell
@Matthew - 你根本没有Join()新线程,是吗?这就可以解释了... - Marc Gravell
1
@Marc Gravell - 我记得我在某个时候加入了线程,但我不确定当我没有使用它时行为是否相同。加入的原因是我想阻止应用程序的其余部分直到工作完成,但也许我可以使用其他方法。 - Matthew Savage
7个回答

12

Invoke是同步的 - 您需要使用Dispatcher.BeginInvoke。此外,我认为您的代码示例应将“SetValue”移动到“else”语句中。


8
我是一个有用的助手,可以将文本翻译成中文。
我认为最好用代码来展示这一点。考虑下面的情景:
线程A执行以下操作:
lock (someObject)
{
   // Do one thing.
   someDispatcher.Invoke(() =>
   {
      // Do something else.
   }
}

线程B执行以下操作:
someDispatcher.Invoke(() =>
{
   lock (someObject)
   {
      // Do something.
   }
}

一开始看上去一切都很好,但实际上却不是这样。这会产生死锁。调度程序就像线程的队列,当处理这样的死锁时,重要的是要以这种方式思考它们:“哪个先前的调度可能会堵塞我的队列?” 线程A将进入...并在锁定下调度。但是,如果B线程在A线程处于标记为“做一件事”的代码点时进入呢?那么...
  • 线程A已经锁定了someObject并正在运行一些代码。
  • 现在线程B进行了调度,并且调度程序将尝试获取someObject的锁,这将导致您的调度程序挂起,因为线程A已经拥有该锁。
  • 然后,线程A将排队另一个调度项。由于调度程序已经被堵塞,因此永远不会触发此项。

现在你遇到了一个美丽的死锁。


感谢您的好解释,省去了我数小时的工作。我通过在调度程序调用中不获取锁(您的B线程所做的)来解决了这个问题。还有其他解决方案吗? - Heribert
@Heribert 这取决于你正在处理的代码。这样的死锁非常特定于应用程序。如果你面临类似上述情况,可以尝试在调度程序调用之外进行锁定。 - Alexandru
谢谢您的帖子和回复,我已经完成了翻译。 - Heribert

6

你说你正在创建一个新的STA线程,这个新线程上的调度程序正在运行吗?

我从“this.Dispatcher.Thread!=Thread.CurrentThread”中得知你希望它是一个不同的调度程序。确保它正在运行,否则它将无法处理其队列。


Keith,这是一个很好的观点。我对调度程序不够熟悉,但是窗口的调度程序不是已经启动了吗?STA线程用于创建新窗口,但是如果我需要手动启动调度程序,那么这就可以解释为什么它没有处理... - Matthew Savage
如果您自己创建STA,请在显示窗口后尝试调用Dispatcher.Run()。我的理解是,Dispatcher是一个消息泵,如果创建了新的UI线程,则会在请求时创建一个Dispatcher;如果您管理创建过程,则必须在Dispatcher上调用Run。 - Keith
1
请查看此帖子:http://eprystupa.wordpress.com/2008/07/28/running-wpf-application-with-multiple-ui-threads/ - Keith

3
我想你是指 if (!this.Dispatcher.CheckAccess())。
我也遇到了使用Invoke时的卡顿,或者如果我使用BeginInvoke,我的委托没有被调用——看起来我已经按照规定做了一切:-(

2

我有一个类似的问题,虽然我还不确定答案是什么,但我认为您的

 if(this.Dispatcher.Thread != Thread.CurrentThread)
{
    this.Dispatcher.Invoke(DispatcherPriority.Normal, (ThreadStart)delegate{
        this.Name = value; // Call same setter, but on the UI thread
    });
    return;
}

应该被替换为

 if(this.Dispatcher.CheckAccess())
{
    this.Dispatcher.Invoke(DispatcherPriority.Normal, (ThreadStart)delegate{
        this.Name = value; // Call same setter, but on the UI thread
    });
    return;
}

CheckAccess不会在Intellisense中显示,但它确实存在并且是为此目的而设计的。此外,我同意通常情况下您需要在这里使用BeginInvoke,但我发现当我异步执行时无法获得UI更新。不幸的是,当我同步执行时,我会遇到死锁条件...


2

这听起来像是死锁;通常情况下,如果调用.Invoke的线程已经持有UI线程需要完成工作的锁/互斥体等资源,就会发生这种情况。最简单的方法是使用BeginInvoke:这样,当前线程可以继续运行,并且很快就会释放锁 - 允许UI线程获取它。或者,如果您可以确定问题所在的锁,您可以有意地释放它一段时间。


谢谢Marc,这个解释很好,但是我仍然不明白为什么一开始就有锁。正如你和Paul建议的那样,BeginInvoke是一个选项,但并不是最优的,不能保证它会完成。这些奇怪的错误真是太疯狂了... - Matthew Savage

0

我知道这是一个旧的线程,但这里有另一个解决方案。

我刚刚解决了一个类似的问题。我的调度程序运行良好,所以...

我必须显示DEBUG -> THREAD WINDOW来识别执行我的代码的所有线程。

通过检查每个线程,我很快就看到哪个线程导致了死锁。

它是多个线程组合一个lock (locker) { ... }语句和对Dispatcher.Invoke()的调用。

在我的情况下,我可以只更改特定的lock (locker) { ... }语句,并将其替换为Interlocked.Increment(ref lockCounter)

这解决了我的问题,因为避免了死锁。

void SynchronizedMethodExample() {

    /* synchronize access to this method */
    if (Interlocked.Increment(ref _lockCounter) != 1) { return; }

    try {
    ...
    }
    finally {
        _mandatoryCounter--;
    }
}

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