事实上,使用Invoke及其相关方法,您无法完全防止在已释放的组件上调用Invoke,或因缺少句柄而导致InvalidOperationException异常。我还没有看到像下面更深入的答案在任何线程中真正解决了这个根本问题,这个问题无法通过预防性测试或使用锁语义来完全解决。
以下是正常的“正确”惯用语:
void OnEventMyUpdate(object sender, MyUpdateEventArgs e)
{
if (!this.IsHandleCreated) return;
if (InvokeRequired)
Invoke(new MyUpdateCallback(this.MyUpdate), e.MyData);
else
this.MyUpdate(e.MyData);
}
void MyUpdate(Object myData)
{
...
}
基本问题:
使用Invoke设施时,将使用Windows消息队列,该队列会将消息放入队列中,以等待或立即触发跨线程调用,就像Post或Send消息一样。如果在Invoke消息之前有一个消息会使组件及其窗口句柄无效,或者该消息刚好放置在您尝试执行任何检查之后,那么您将会遇到麻烦。
x thread -> PostMessage(WM_CLOSE); // put 'WM_CLOSE' in queue
y thread -> this.IsHandleCreated // yes we have a valid handle
y thread -> this.Invoke(); // put 'Invoke' in queue
ui thread -> this.Destroy(); // Close processed, handle gone
y thread -> throw Invalid....() // 'Send' comes back, thrown on calling thread y
没有真正的方式可以知道控件将要从队列中移除自己,也没有任何合理的方法可以“撤销”调用。无论您进行多少检查或额外的锁定,都无法阻止其他人发出类似关闭或停用的操作。这种情况有很多。
一种解决方案:
首先要意识到的是,调用将失败,就像(IsHandleCreated)检查会忽略事件一样。如果目标是保护非UI线程上的调用方,则需要处理异常,并将其视为未成功的任何其他调用(以避免应用程序崩溃或执行其他操作)。除非你打算重写/重新生成调用设施,否则catch是你唯一的方法来知道。
void OnEventMyWhatever(object sender, MyUpdateEventArgs e)
{
if (!this.IsHandleCreated) return;
if (InvokeRequired)
{
try
{
Invoke(new MyUpdateCallback(this.MyUpdate), e.MyData);
}
catch (InvalidOperationException ex)
{
if (this.IsHandleCreated) throw;
}
}
else
{
this.MyUpdate(e.MyData);
}
}
void MyUpdate(Object myData)
{
...
}
异常过滤器可以根据需要进行定制。需要注意的是,工作线程通常没有像UI线程那样完善的外部异常处理和日志记录,因此您可能希望在工作端口只消耗掉任何异常。或者记录并重新抛出所有异常。对于许多人来说,工作线程中未捕获的异常意味着应用程序将崩溃。