C#: 方法Invoke永远不会返回

3

我有一个线程调用,但它从不返回。

线程一直运行得很好,直到我调用这一行代码:owner.Invoke(methInvoker);

调试时,我可以慢慢地单步执行,但一旦我触及owner.Invoke,就结束了!

Control owner;
public event ReportCeProgressDelegate ProgressChanged;

public void ReportProgress(int step, object data) {
  if ((owner != null) && (ProgressChanged != null)) {
    if (!CancellationPending) {
      ThreadEventArg e = new ThreadEventArg(step, data);
      if (owner.InvokeRequired) {
        MethodInvoker methInvoker = delegate { ProgressChanged(this, e); };
        owner.Invoke(methInvoker);
      } else {
        ProgressChanged(this, e);
      }
    } else {
      mreReporter.Set();
      mreReporter.Close();
    }
  }
}

注意:这是一个自定义类,模仿了BackgroundWorker类,但不适用于没有表单控件的情况。

考虑到可能不需要使用Invoke,我手动在调试器中跟踪代码执行,在那部分代码上尝试直接调用ProgressChanged方法,但是VS2010的调试器抛出了一个线程交叉异常。

编辑:

根据我收到的前三条评论,我想更新一下我的ProgressChanged方法:

worker.ProgressChanged += delegate(object sender, ThreadEventArg e) {
  if (progressBar1.Style != ProgressBarStyle.Continuous) {
    progressBar1.Value = 0;
    object data = e.Data;
    if (data != null) {
      progressBar1.Maximum = 100;
    }
    progressBar1.Style = ProgressBarStyle.Continuous;
  }
  progressBar1.Value = e.ProgressPercentage;
};

第一行的匿名方法上设置了断点,但它也从未被触发。

编辑 2

下面是对线程调用的更完整清单:

List<TableData> tList = CollectTablesFromForm();
if (0 < tList.Count) {
  using (SqlCeReporter worker = new SqlCeReporter(this)) {
    for (int i = 0; i < tList.Count; i++) {
      ManualResetEvent mre = new ManualResetEvent(false);
      worker.StartThread += SqlCeClass.SaveSqlCeDataTable;
      worker.ProgressChanged += delegate(object sender, ThreadEventArg e) {
        if (progressBar1.Style != ProgressBarStyle.Continuous) {
          progressBar1.Value = 0;
          object data = e.Data;
          if (data != null) {
            progressBar1.Maximum = 100;
          }
          progressBar1.Style = ProgressBarStyle.Continuous;
        }
        progressBar1.Value = e.ProgressPercentage;
      };
      worker.ThreadCompleted += delegate(object sender, ThreadResultArg e) {
        Cursor = Cursors.Default;
        progressBar1.Visible = false;
        progressBar1.Style = ProgressBarStyle.Blocks;
        if (e.Error == null) {
          if (e.Cancelled) {
            MessageBox.Show(this, "Save Action was Cancelled.", "Save Table " + tList[i].TableName);
          }
        } else {
          MessageBox.Show(this, e.Error.Message, "Error Saving Table " + tList[i].TableName, MessageBoxButtons.OK, MessageBoxIcon.Error);
        }
        mre.Set();
      };
      worker.RunWorkerAsync(tList[i]);
      progressBar1.Value = 0;
      progressBar1.Style = ProgressBarStyle.Marquee;
      progressBar1.Visible = true;
      Cursor = Cursors.WaitCursor;
      mre.WaitOne();
    }
  }
}

希望这不会过度!我讨厌呈现太多信息,因为那样我就会被人批评我的风格。 :)


调用的方法完全是一个不同的过程,在你的方法开始处添加断点,这样你就能进入它了。 - Senad Meškin
事件循环可能被绑定在其他地方。我会检查另一个处理程序中是否有任何阻塞。 - Brook
你是否在调试器卡住时暂停了它,进入“线程”调试窗口,双击每个线程,查看其调用堆栈和执行位置,并查找1)死锁(例如,代码在一个以上的位置上暂停在“Wait”或“WaitAll”上)2)一般奇怪的操作。在应用程序冻结之前,请注意GUI线程的线程ID,特别是在冻结后它正在做什么。 - David Gladfelter
2个回答

4
  worker.RunWorkerAsync(tList[i]);
  //...
  mre.WaitOne();

这是一个保证死锁的情况。你传递给Control.Begin/Invoke()的委托只有在UI线程空闲时才能运行,重新进入消息循环。你的UI线程不是空闲的,它被WaitOne()调用阻塞了。该调用无法完成,直到你的工作线程完成。你的工作线程无法完成,直到Invoke()调用完成。该调用无法完成,直到UI线程变为闲置状态。死锁城市。

阻塞UI线程基本上是错误的做法。不仅因为.NET管道,COM已经要求它永远不要阻塞。这就是BGW有一个RunWorkerCompleted事件的原因。


这正是Brian的评论让我也这么想。看起来我需要重新设计我的类的某些部分。 - user153923
1
好的,只需删除mre.WaitOne()调用。然后开始考虑如何处理“工作已完成”场景。你有点注定要重新发明BGW。 - Hans Passant

1

你可能已经死锁了UI和工作线程。Control.Invoke通过将委托的执行封送到UI线程上,向UI线程的消息队列发布一条消息,然后等待该消息被处理,这意味着在Control.Invoke返回之前,委托的执行必须完成。但是,如果您的UI线程正在忙于除了分派和处理消息之外的其他事情呢?我可以从您的代码中看到一个ManualResetEvent可能正在发挥作用。你的UI线程是否碰巧在调用WaitOne时被阻塞了?如果是这样,那肯定是问题所在。由于WaitOne不会泵送消息,它将阻塞UI线程,当从工作线程调用Control.Invoke时,这将导致死锁。

如果你想让你的ProgressChanged事件像BackgroundWorker一样工作,那么你需要调用Control.Invoke将这些事件处理程序放到UI线程上。这就是BackgroundWorker的工作方式。当然,你不必完全模仿BackgroundWorker类在这方面的行为,只要你准备好在处理ProgressChanged事件时让调用者自己进行封送处理即可。

第一个代码块中引用的ManualResetEvent仅限于自定义线程类...到目前为止,我还没有真正使用它。如果需要,它似乎是取消非常漫长的过程的好方法。另一方面,主线程(UI)也有自己的ManualResetEvent。 该代码块的任务是在表单上保存1个或多个数据表,并且我希望防止同时将多个表保存到本地SQL CE数据库中。请参见EDIT 2以获取该代码(稍后发布)。 - user153923

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接