【问题标题】:C#: Method Invoke never returnsC#:方法调用永不返回
【发布时间】:2011-07-01 15:23:25
【问题描述】:

我有一个永远不会返回的线程调用调用。

线程一直运行良好,直到我以这种方式调用该行,“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的调试器抛出了跨线程异常。

编辑:

由于我收到了前 3 个 cmets,我想用我的 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();
    }
  }
}

我希望这不是矫枉过正!我讨厌提供太多信息,因为那样我会让人们批评我的风格。 :)

【问题讨论】:

  • 被调用的方法是完全不同的过程,在方法的开头添加断点,这样你就可以进入它了。
  • 事件循环可以绑定到其他地方。我会检查另一个处理程序中是否有任何阻塞。
  • 你有没有在调试器挂起时暂停它,进入“线程”调试窗口,双击每个线程,查看它的调用堆栈和执行位置并寻找 1) 死锁 (例如,代码在多个地方的“Wait”或“WaitAll”处暂停)2)一般奇怪的操作。记下应用程序冻结前 GUI 线程的线程 ID,尤其是在冻结后查看它在做什么。

标签: c# multithreading backgroundworker invoke


【解决方案1】:
  worker.RunWorkerAsync(tList[i]);
  //...
  mre.WaitOne();

这是一个有保证的死锁。您传递给 Control.Begin/Invoke() 的委托只能在 UI 线程空闲且重新进入消息循环时运行。您的 UI 线程不是空闲的,它在 WaitOne() 调用中被阻塞。在您的工作线程完成之前,该调用无法完成。在 Invoke() 调用完成之前,您的工作线程无法完成。在 UI 线程空闲之前,该调用无法完成。死锁城市。

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

【讨论】:

  • 这也是 Brian 的评论让我想到的。看来我需要重新设计课堂的某些部分。
  • 好吧,只需删除 mre.WaitOne() 调用。然后开始考虑你想如何处理“工人完成”的场景。你有点注定要重新发明 BGW。
【解决方案2】:

您很可能已经死锁了 UI 和工作线程。 Control.Invoke 通过将消息发布到 UI 线程的消息队列,将委托的执行编组到 UI 线程,然后等待该消息被处理,这反过来意味着委托的执行必须在 Control.Invoke 返回之前完成。但是,如果您的 UI 线程忙于分派和处理消息之外的其他事情怎么办?我可以从您的代码中看到ManualResetEvent 可能在这里起作用。您的 UI 线程是否在偶然调用 WaitOne 时被阻塞?如果是这样,那肯定是问题所在。由于WaitOne 不发送消息,它会阻塞 UI 线程,当从工作线程调用 Control.Invoke 时会导致死锁。

如果您希望 ProgressChanged 事件的行为与 BackgroundWorker 一样,那么您需要调用 Control.Invoke 以将这些事件处理程序放到 UI 线程上。这就是BackgroundWorker 的工作方式。当然,只要您准备好让调用者在处理 ProgressChanged 事件时进行自己的编组,您就必须在这方面完全模仿 BackgroundWorker 类。

【讨论】:

  • 第一个代码块中引用的ManualResetEvent 仅本地化为自定义线程类......到目前为止,我还没有真正使用它。如果我需要,这似乎是取消一个非常漫长的过程的好方法。另一方面,主线程 (UI) 也有自己的 ManualResetEvent。代码块的任务是在表单上保存 1 个或多个数据表,我想防止将多个表同时保存到本地 SQL CE 数据库。请参阅 EDIT 2 了解该代码(稍后发布)。
猜你喜欢
  • 2019-11-13
  • 2014-09-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多