5

我有一个 UserControl,上面有一个名为 mTreeView 的 TreeView 控件。我可以从多个不同的线程获取数据更新,这些会导致 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 分支被占用了!就好像在采用 else 分支之后,在不同的线程上将 InvokeRequired 设置为 true。

我的方法有什么问题吗?我能做些什么来防止这种情况发生?

编辑:我的同事告诉我,问题是在创建控件之前 InvokeRequired 为假,所以这就是它在启动时发生的原因。不过,他不确定该怎么做。有任何想法吗?

4

3 回答 3

7

这是一个标准的线程比赛。在创建 TreeView 之前,您过早地启动线程。因此,您的代码将 InvokeRequired 视为 false 并在稍后创建本机控件时失败。通过仅在窗体的 Load 事件触发时才启动线程来解决此问题,这是保证所有控制句柄都有效的第一个事件。

顺便说一句,代码中的一些误解。不需要使用,InvokeRequired 和 Begin/Invoke 都是线程安全的。InvokeRequired 是一种反模式。您几乎总是知道该方法将由工作线程调用。所以使用 InvokeRequired 只在它为 false 时抛出异常。这将允许及早诊断此问题。

于 2012-07-13T16:06:16.523 回答
2

当您编组回 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不会返回,而EventHandler在b为false之前不会返回......

请注意我使用 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 方法。

于 2012-07-13T15:49:16.640 回答
1

您在上面显示的模式对我来说看起来 100% 没问题(尽管有一些额外的不必要的锁定,但是我看不出这将如何导致您描述的问题)。

正如 David W 指出的那样,您所做的与此扩展方法之间的唯一区别是您直接mTreeView在 UI 线程上访问,而不是将其作为参数传递给您的操作,但是只有在mTreeView更改,并且在任何情况下,您都必须相当努力才能导致您所描述的问题。

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

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

于 2012-07-13T15:46:27.323 回答