5

我有一个表单,它“监听”在其他地方引发的事件(不是在表单本身,也不是它的一个子控件)。事件由即使在 Form 被释放后仍然存在的对象引发,并且可能在创建 Form 句柄的线程之外的线程中引发,这意味着我需要在事件处理程序中执行 Invoke(以显示更改形式,例如)。

Dispose(bool)表单(重写)的方法中,我取消了在调用此方法时可能仍订阅的所有事件。但是,有时仍会从其中一个事件处理程序调用 Invoke。我认为这是因为事件处理程序在事件取消订阅前一刻被调用,然后操作系统将控制权切换到执行的 dispose 方法,然后将控制权返回给调用已处理对象上的 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);
 }
}

更新(根据艾伦的要求):

我从不显式调用 Dispose 方法,我让框架来完成。到目前为止,死锁仅在应用程序关闭时发生。在我进行锁定之前,我有时会在表单关闭时抛出一些异常。

4

3 回答 3

3

有两种方法需要考虑。一种是在 内部有一个锁定对象Form,并且内部调用DisposeBeginInvoke调用发生在锁定内;因为既不Dispose也不BeginInvoke应该花费很长时间,代码永远不必等待很长时间才能获得锁。

另一种方法是仅仅声明由于Control.BeginInvoke/中的设计错误Form.BeginInvoke,这些方法有时会抛出实际上无法防止的异常,并且应该在动作是否发生在表单上并不重要的情况下简单地被吞下反正已经被处理掉了。

于 2012-07-05T23:45:06.763 回答
0

我想为 supercat 的答案提供一种可能很有趣的附录。

首先创建一个初始计数为 1 的 CountdownEvent(我们将其称为 _invoke_counter)。这应该是表单(或控件)本身的成员变量:

private readonly CountdownEvent _invoke_counter = new CountdownEvent(1);

将 Invoke/BeginInvoke 的每次使用包装如下:

if(_invoke_counter.TryAddCount())
{
    try
    {
        //code using Invoke/BeginInvoke goes here
    }
    finally { _invoke_counter.Signal(); }
}

然后在您的 Dispose 中,您可以执行以下操作:

_invoke_counter.Signal();
_invoke_counter.Wait();

这也允许你做一些其他的好事。CountdownEvent.Wait() 函数有一个超时过载。也许您只想等待一段时间让调用函数完成,然后再让它们死掉。如果您希望调用需要很长时间才能完成,您还可以使用 DoEvents() 在循环中执行类似于 Wait(100) 的操作,以保持响应。使用这种方法可以实现很多漂亮的东西。

这应该可以防止任何奇怪的计时竞争条件类型的问题,并且理解和实现起来相当简单。如果有人对此有任何明显的问题,我很想听听他们,因为我在生产软件中使用了这种方法。

重要提示:确保处理代码在终结器的线程上(它应该在“自然”处理中)。如果您尝试从 UI 线程手动调用 Dispose() 方法,它会死锁,因为它会卡在 _invoke_counter.Wait(); 并且调用不会运行,等等。

于 2013-07-03T13:13:02.460 回答
0

我在多线程时遇到了 Invoke 方法的问题,我找到了一个像魅力一样工作的解决方案!

我想在更新表单上的标签以进行监视的任务中创建一个循环。

但是当我关闭表单窗口时,我的 Invoke 抛出了一个异常,因为我的 Form 被释放了!

这是我为解决此问题而实施的模式:

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)并调用对象Task的Cancel方法来停止线程。

然后我访问 ThrowIfCancellationRequested() 方法,该方法抛出一个 OperationCanceledException 允许,就在之后,退出循环并完成任务。

Invoke 方法在队列中发送“窗口消息”。

微软说: « 对于每个创建窗口的线程,操作系统都会为窗口消息创建一个队列。»

所以我调用了另一个方法,它现在将真正关闭窗口,但这次使用 Invoke 方法来确保该消息将是队列的最后一条!

And then I close the window with the Dispose() method.

于 2016-06-08T14:10:18.870 回答