C# 奇怪的问题

C# 奇怪的问题,c#,winforms,multithreading,C#,Winforms,Multithreading,我有一个UserControl,上面有一个名为mTreeView的TreeView控件。我可以从多个不同的线程获取数据更新,这些线程会导致树视图被更新。为此,我设计了以下模式: 所有数据更新事件处理程序必须获取锁,然后检查是否需要调用;如果是这样,则通过调用Invoke来完成这项工作。以下是相关代码: public partial class TreeViewControl : UserControl { object mLock = new object(); vo

我有一个UserControl,上面有一个名为mTreeView的TreeView控件。我可以从多个不同的线程获取数据更新,这些线程会导致树视图被更新。为此,我设计了以下模式: 所有数据更新事件处理程序必须获取锁,然后检查是否需要调用;如果是这样,则通过调用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是从一个与它创建的线程不同的线程更新的。我回到调用堆栈中的lock和invoke,你瞧,c.InvokeRequired是真的,但是else分支被执行了!这就好像在执行else分支后,InvokeRequired在另一个线程上被设置为true

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


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

您在上面展示的模式在我看来100%不错(尽管有一些额外的不必要的锁定,但是我看不出这会导致您描述的问题)

正如David W所指出的,您所做的与所做的唯一不同之处在于,您直接在UI线程上访问
mTreeView
,而不是将其作为参数传递给您的操作,然而,这只会在
mTreeView
的值发生变化时产生影响,在任何情况下,你都必须相当努力地让它产生你所描述的问题

这意味着问题一定是别的


我能想到的唯一一件事是,您可能已经在UI线程以外的线程上创建了
mTreeView
——如果是这种情况,那么访问树视图将是100%安全的,但是,如果您尝试将该树视图添加到另一个线程上创建的表单中,那么它将出现与您描述的异常类似的异常。

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

Invoke的问题是它阻塞了调用线程。调用线程通常不关心在UI线程上完成了什么。在这种情况下,我建议使用BeginInvoke异步将操作封送回UI线程。在某些情况下,后台线程可能会在调用时被阻塞,而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来停止某些代码段的运行。这与lock非常相似。所以,是的,您可以通过使用lock来结束死锁

只需这样做:

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方法。

这是一个标准的线程竞赛。在创建TreeView之前,您启动线程太快。因此,您的代码将InvokeRequired视为false,并在瞬间创建本机控件时失败。要解决这个问题,只需在窗体的Load事件触发时启动线程,这是第一个确保所有控件句柄有效的事件


顺便说一句,代码中存在一些错误概念。不需要使用锁,InvokeRequired和Begin/Invoke都是线程安全的。invokererequired是一个反模式。您几乎总是知道该方法将由工作线程调用。因此,使用InvokeRequired仅在异常为false时抛出异常。这将允许尽早诊断此问题。

因为有多个不同的线程调用这些事件处理程序;我只希望一次执行一个。不需要锁-此方法的目的是封送对UI线程的所有调用,因此根据定义,一次只能有一个线程执行此操作(UI线程)。关于不需要锁,您肯定是对的,但这并不能解释为什么会出现这种异常。我想知道问题是否不是因为在LockAndInvoke方法中传递的匿名函数没有引用mTreeView本身,在您可以执行调用之前,它最终被调用到UI线程以外的其他对象上。只是使用多个线程进行了多次测试,我无法复制我100%确定mTreeView及其父窗体(以及我的应用程序的所有UI控件)是在UI线程上创建的。@Dr_Asik您能发布实例化
mTreeView
的代码吗?我在代码中看不到任何
InitializeComponent()
。@ken2k mTreeView由设计器生成的代码实例化,InitializeComponent()由构造函数调用。构造函数是从实例化表单的UI线程调用的;不过,要在简短的评论中解释原因有点困难。无论如何谢谢你!关于“use InvokeRequired only to throw a exception when is false”:我这样做了,每次(启动时)它都会抛出异常,即使我在Load事件上启动了工作线程。不过,在调用之后立即调用Invoke仍然有效!看起来InvokeRequired将在一段时间内继续报告false,即使调用Invoke是合法的。我真的不知道为什么。事实证明,这些事件处理程序在启动时是从UI线程调用的:订阅代码(我无法控制)出于某种原因这样做的。因此,在这种情况下,我确实需要检查invokererequired,因为可以从UI和工作线程调用代码