C# 如何正确地处理表单,而不存在从已处理对象上的另一个线程调用调用的风险?
我有一个窗体,它“侦听”在其他地方引发的事件(不在窗体本身上,也不在其子控件上)。事件是由存在的对象引发的,即使在表单被释放之后,这些对象仍然存在,并且可能在创建表单句柄的线程之外的线程中引发,这意味着我需要在事件处理程序中进行调用(例如,显示表单上的更改) 在表单(重写)的C# 如何正确地处理表单,而不存在从已处理对象上的另一个线程调用调用的风险?,c#,winforms,multithreading,events,dispose,C#,Winforms,Multithreading,Events,Dispose,我有一个窗体,它“侦听”在其他地方引发的事件(不在窗体本身上,也不在其子控件上)。事件是由存在的对象引发的,即使在表单被释放之后,这些对象仍然存在,并且可能在创建表单句柄的线程之外的线程中引发,这意味着我需要在事件处理程序中进行调用(例如,显示表单上的更改) 在表单(重写)的Dispose(bool)方法中,我取消订阅了调用此方法时仍可能订阅的所有事件。但是,有时仍然会从一个事件处理程序调用Invoke。我假设这是因为在取消订阅事件之前,事件处理程序刚刚被调用,然后OS将控制权切换到执行的dis
Dispose(bool)
方法中,我取消订阅了调用此方法时仍可能订阅的所有事件。但是,有时仍然会从一个事件处理程序调用Invoke。我假设这是因为在取消订阅事件之前,事件处理程序刚刚被调用,然后OS将控制权切换到执行的dispose方法,然后将控制权返回给调用disposed对象的Invoke方法的处理程序
锁定线程没有帮助,因为调用Invoke将锁定调用线程,直到主线程处理调用的方法。这可能永远不会发生,因为主线程本身可能正在等待调用线程已获取的对象上的锁的释放,从而创建死锁
因此,简而言之,当表单订阅了外部事件(可能在不同线程中引发)时,如何正确地处理表单
下面是一些关键方法目前的情况。这种方法遇到了我上面描述的问题,但我不确定如何纠正它们
这是一个处理模型数据部分更改的事件处理程序:
private void updateData()
{
if (model != null && model.Data != null)
{
model.Data.SomeDataChanged -= new MyEventHandler(updateSomeData);
model.Data.SomeDataChanged += new MyEventHandler(updateSomeData);
}
updateSomeData();
}
这是一个事件处理程序,必须对视图进行更改:
private void updateSomeData()
{
if (this.InvokeRequired) this.myInvoke(new MethodInvoker(updateSomeData));
else
{
// do the necessary changes
}
}
以及myInvoke方法:
private object myInvoke(Delegate method)
{
object res = null;
lock (lockObject)
{
if (!this.IsDisposed) res = this.Invoke(method);
}
return res;
}
我对Dispose(bool)
方法的重写:
protected override void Dispose(bool disposing)
{
lock (lockObject)
{
if (disposing)
{
if (model != null)
{
if (model.Data != null)
{
model.Data.SomeDataChanged -= new MyEventHandler(updateSomeData);
}
// unsubscribe other events, omitted for brevity
}
if (components != null)
{
components.Dispose();
}
}
base.Dispose(disposing);
}
}
更新(根据Alan的要求):
我从不显式调用Dispose方法,我让框架来完成。到目前为止,死锁只在应用程序关闭时发生。在执行锁定之前,有时在简单关闭窗体时会抛出一些异常。 < P>有两种方法需要考虑。一种是在
表单
中有一个锁定对象,并在锁中发生对Dispose
和BeginInvoke
的内部调用;由于Dispose
和BeginInvoke
都不需要很长时间,因此代码不必等待很长时间才能锁定
另一种方法是声明,由于
控件.BeginInvoke
/表单.BeginInvoke
中的设计错误,这些方法有时会抛出一个实际上无法预防的异常,如果操作是否发生在已经处理过的表单上并不重要,那么应该简单地将其吞并。我想为supercat的答案提供一种可能很有趣的补充
首先制作一个初始计数为1的倒计时事件(我们称之为_invoke_counter)。这应该是表单(或控件)本身的成员变量:
按如下方式包装每次使用Invoke/BeginInvoke:
if(_invoke_counter.TryAddCount())
{
try
{
//code using Invoke/BeginInvoke goes here
}
finally { _invoke_counter.Signal(); }
}
然后,在您的配置中,您可以执行以下操作:
_invoke_counter.Signal();
_invoke_counter.Wait();
这也允许你做一些其他的好事。CountdownEvent.Wait()函数有一个带超时的重载。也许您只想等待一段时间,让调用函数在终止之前完成。如果预期调用需要很长时间才能完成,还可以使用DoEvents()在循环中执行Wait(100)之类的操作来保持响应。用这种方法你可以达到很多漂亮的效果
这应该可以防止任何奇怪的计时竞争条件类型的问题,而且理解和实现起来相当简单。如果有人看到这种方法有任何明显的问题,我很想听听,因为我在生产软件中使用这种方法
重要提示:确保处理代码位于终结器的线程上(它应该处于“自然”处理中)。如果您尝试从UI线程手动调用Dispose()方法,它将死锁,因为它将卡在_invoke_counter.Wait()上;调用将不会运行,等等。我在多线程处理时遇到了调用方法的问题,我找到了一个非常有效的解决方案 我想在任务中创建一个循环,更新表单上的标签以进行监视 但是当我关闭窗体窗口时,我的调用抛出了一个异常,因为我的窗体已被释放 以下是我为解决此问题而实现的模式:
class yourClass : Form
{
private bool isDisposed = false;
private CancellationTokenSource cts;
private bool stopTaskSignal = false;
public yourClass()
{
InitializeComponent();
this.FormClosing += (s, a) =>
{
cts.Cancel();
isDisposed = true;
if (!stopTaskSignal)
a.Cancel = true;
};
}
private void yourClass_Load(object sender, EventArgs e)
{
cts = new CancellationTokenSource();
CancellationToken token = cts.Token;
Task.Factory.StartNew(() =>
{
try
{
while (true)
{
if (token.IsCancellationRequested)
{
token.ThrowIfCancellationRequested();
}
if (this.InvokeRequired)
{
this.Invoke((MethodInvoker)delegate { methodToInvoke(); });
}
}
}
catch (OperationCanceledException ex)
{
this.Invoke((MethodInvoker)delegate { stopTaskSignalAndDispose(); });
}
}, token);
}
public void stopTaskSignalAndDispose()
{
stopTaskSignal = true;
this.Dispose();
}
public void methodToInvoke()
{
if (isDisposed) return;
label_in_form.Text = "text";
}
}
我在调用中执行methodToInvoke(),以从窗体线程更新标签
关闭窗口时,将调用FormClosing事件。我借此机会取消关闭窗口(a.cancel),并调用对象任务的cancel方法来停止线程
然后,我访问throwifcancellationrequest()方法,该方法抛出一个OperationCanceledException,允许退出循环并完成任务
Invoke方法在队列中发送“窗口消息”
Microsoft说:«对于每个创建窗口的线程,操作系统都会为窗口消息创建一个队列。»
因此,我调用了另一个方法,该方法现在将真正关闭窗口,但这次使用Invoke方法确保此消息将是队列的最后一条
然后我用Dispose()方法关闭窗口。无论您如何尝试切片,这看起来都很痛苦。。。你能用一些关于你为什么需要处理表格的信息来更新这个问题吗?可能有另一种解决方案。检查可调用方法中的diposing,而不是disposed,maybe@TonyHopkinson,我想我不明白。你的意思是我应该在处理程序中执行
if(this.Disposing)
,或者在myInvoke
方法中?在被调用的方法中,可能重复@NikolaNovak。好吧,让我们假设一下
class yourClass : Form
{
private bool isDisposed = false;
private CancellationTokenSource cts;
private bool stopTaskSignal = false;
public yourClass()
{
InitializeComponent();
this.FormClosing += (s, a) =>
{
cts.Cancel();
isDisposed = true;
if (!stopTaskSignal)
a.Cancel = true;
};
}
private void yourClass_Load(object sender, EventArgs e)
{
cts = new CancellationTokenSource();
CancellationToken token = cts.Token;
Task.Factory.StartNew(() =>
{
try
{
while (true)
{
if (token.IsCancellationRequested)
{
token.ThrowIfCancellationRequested();
}
if (this.InvokeRequired)
{
this.Invoke((MethodInvoker)delegate { methodToInvoke(); });
}
}
}
catch (OperationCanceledException ex)
{
this.Invoke((MethodInvoker)delegate { stopTaskSignalAndDispose(); });
}
}, token);
}
public void stopTaskSignalAndDispose()
{
stopTaskSignal = true;
this.Dispose();
}
public void methodToInvoke()
{
if (isDisposed) return;
label_in_form.Text = "text";
}
}