奇怪的InvokeRequired问题

时间:2012-07-13 15:24:50

标签: c# winforms multithreading

我有一个带有TreeView控件的UserControl,名为mTreeView。我可以从多个不同的线程获取数据更新,这些更新会导致TreeView更新。为此,我设计了以下模式: 所有数据更新事件处理程序必须获取一个锁,然后检查InvokeRequired;如果是这样,请通过调用Invoke来完成工作。这是相关的代码:

  public partial class TreeViewControl : UserControl
  {  
    object mLock = new object();
    void LockAndInvoke(Control c, Action a)
    {
      lock (mLock)
      {
        if (c.InvokeRequired)
        {
          c.Invoke(a);
        }
        else
        {
          a();
        }
      }
    }

    public void DataChanged(object sender, NewDataEventArgs e)
    {
      LockAndInvoke(mTreeView, () =>
        {
          // get the data
          mTreeView.BeginUpdate();
          // perform update
          mTreeView.EndUpdate();
        });
    }    
  }

我的问题是,有时候,在启动时,我会在mTreeView.BeginUpdate()上得到一个InvalidOperationException,说mTreeView是从不同于创建它的线程更新的。我在调用堆栈中返回到我的LockAndInvoke,并且看,c.InvokeRequired是真的但是其他分支已被占用!就像在调用else分支后,在另一个线程上将InvokeRequired设置为true一样。

我的方法有什么问题,我该怎么做才能防止这种情况发生?

编辑:我的同事告诉我,问题是InvokeRequired在创建控件之前是假的,所以这就是它在启动时发生的原因。但他不知道该怎么办。有什么想法吗?

3 个答案:

答案 0 :(得分:7)

这是标准的穿线比赛。在创建TreeView之前,您将很快启动该线程。因此,您的代码将InvokeRequired视为false,并在创建本机控件后一瞬间失败。通过仅在表单的Load事件触发时启动线程来修复此问题,第一个事件保证所有控件句柄都有效。

代码中的一些误解btw。不需要使用 lock ,InvokeRequired和Begin / Invoke都是线程安全的。而InvokeRequired是一种反模式。您几乎总是知道该方法将由工作线程调用。因此,只使用InvokeRequired在异常时抛出异常。哪个可以尽早诊断出这个问题。

答案 1 :(得分:1)

上面显示的模式对我来说看起来很好(虽然有一些额外的不必要的锁定,但是我无法看到这会导致你所描述的问题)。

正如David W指出的那样,你正在做的事情和this extension method之间的唯一区别是你直接访问UI线程上的mTreeView而不是将其作为参数传递给你的行为,但是如果mTreeView的值发生变化,这只会产生影响,而且无论如何你都必须努力尝试才能解决问题。

这意味着问题必须是其他问题。

我唯一能想到的是你可能在UI线程以外的线程上创建了mTreeView - 如果是这种情况,那么访问树视图将是100%安全的,但是如果你尝试将该树视图添加到在不同线程上创建的表单,然后它会发出类似于您描述的异常的异常。

答案 2 :(得分:1)

当你编组回UI线程时,它是一个线程 - 它一次只能做一件事。调用Invoke时不需要任何锁定。

Invoke的问题是它阻塞了调用线程。调用线程通常不关心在UI线程上完成什么。在这种情况下,我建议使用BeginInvoke将操作异步编组回UI线程。在某些情况下,可以在Invoke上阻止后台线程,而UI线程可以等待后台线程完成某些操作并最终导致死锁:例如:

private bool b;
public void EventHandler(object sender, EventArgs e)
{
  while(b) Thread.Sleep(1); // give up time to any other waiting threads
  if(InvokeRequired)
  {
    b = true;
    Invoke((MethodInvoker)(()=>EventHandler(sender, e)), null);
    b = false;
  }
}

...上面的内容会在while循环中死锁,因为在调用EventHandler返回之前Invoke不会返回,并且在b为false之前EventHandler不会返回...

请注意我使用bool来阻止某些代码段运行。这与锁非常相似。所以,是的,你可以通过使用锁来结束僵局。

只需这样做:

public void DataChanged(object sender, NewDataEventArgs e)
{
      if(InvokeRequired)
      {
          BeginInvoke((MethodInvoker)(()=>DataChanged(sender, e)), null);
          return;
      }
      // get the data
      mTreeView.BeginUpdate();
      // perform update
      mTreeView.EndUpdate();
}

这只是在UI线程上异步重新调用DataChanged方法。