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