关闭主窗体前关闭侧边线程

5
我正在使用Visual Studio .NET 2010和C#制作一个带有Windows窗体的GUI应用程序。 我有多个线程,例如: 1. 检查目标设备是否断开连接的线程 2. 请求并接收设备测量结果的线程 3. 不断更新主窗体的线程。此线程使用Control.Invoke()调用主窗体控件的更改。 我的目标:当用户在主窗体上按(x)时,我希望确保窗体被正确关闭。这意味着我希望我的连接关闭,所有侧面线程终止等等。 我尝试使用FormClosing事件。在那里,我将KillThreads标志设置为true,并等待侧面线程终止。每个线程都有一个检查,类似于
if(KillThreads)
    return;

但是存在一个问题。如果我尝试等待所有线程终止。
for(;;)
if(ActiveSideThreads == 0)
    break;
closeConnection();

线程N3被Invoke()调用冻结了; 当然,这是因为主GUI线程在无限循环中。 所以表单关闭被永远延迟了。
如果我将关闭前操作实现为线程过程4,并在FromClosing事件处理程序中创建一个新线程, 表单将立即关闭,在线程4终止之前,Invoke()会抛出一个错误- t. 4无法及时关闭线程3。 如果我添加Join(), Invoke()调用,又会再次阻塞,线程3永远不会终止,因此线程4将永久等待。
我该怎么办? 有什么方法可以解决这个问题吗?

2
如果您关闭主窗体,这意味着您正在关闭应用程序,对吗?因此,请查看System.Windows.Forms.Application.Exit()方法,它会在退出之前停止所有消息循环和所有运行中的线程。 - sll
为什么在关闭窗体之前你想/需要等待这些线程退出? - Damien_The_Unbeliever
也可以将线程对象保存在列表中,在关闭主窗体之前使用[线程对象].kill()或类似的方法。 - Rev
@sll,我需要在主窗体关闭之前关闭我的线程,因为其中一些线程使用了Invoke()。 - HtonS
@Rev,其中一个线程与外部设备进行通信,通常是命令的发送/接收。如果在命令传输过程中(例如3个字节中的1个字节)线程被杀死,可能会导致设备停止工作。因此,我更喜欢不使用蛮力方法。 - HtonS
显示剩余2条评论
4个回答

7
首先,确定您是否真的需要使用Invoke。从我个人的角度来看,Invoke是在工作线程启动后触发 UI 线程上的操作最常用的技术之一,但这种方法已经被过度使用了一段时间。另一种方法是在工作线程中创建某种类型的消息对象,描述需要在 UI 线程上执行的操作,并将其放入共享队列中。然后,UI 线程将使用 System.Windows.Forms.Timer 在某个间隔内轮询此队列,并导致每个操作发生。这样做有几个优点。
  • 它打破了Invoke强加的 UI 和工作线程之间的紧密耦合。
  • 它将更新 UI 线程的责任放在 UI 线程上,这本应该是它所属的地方。
  • UI 线程可以决定何时以及多久更新。
  • 没有 UI 消息泵被淹没的风险,就像由工作线程引发的调度技术那样。
  • 工作线程无需等待确认更新已执行,然后再继续下一步(即,您可以在 UI 和工作线程上获得更高的吞吐量)。
  • 它更容易处理关闭操作,因为您几乎可以消除请求工作线程优雅地终止时通常存在的所有竞争条件。
当然,Invoke非常有用,有许多理由继续使用这种方法。如果您决定继续使用Invoke,请继续阅读。
一个想法是在Form.Closing事件中设置KillThreads标志,然后通过设置FormClosingEventArgs.Cancel = true取消关闭窗体。您可能希望告知用户已经请求并正在进行关闭操作。您可能希望禁用表单上的某些控件,以便不能启动新操作。因此,我们基本上是在发出关闭请求,但在工作线程首先关闭之前推迟了窗体的关闭。为此,您可能需要启动一个定时器,定期检查工作线程是否已结束,如果已结束,则可以调用Form.Close
public class YourForm : Form
{
  private Thread WorkerThread;
  private volatile bool KillThreads = false;

  private void YourForm_Closing(object sender, FormClosingEventArgs args)
  {
    // Do a fast check to see if the worker thread is still running.
    if (!WorkerThread.Join(0))
    {
      args.Cancel = true; // Cancel the shutdown of the form.
      KillThreads = true; // Signal worker thread that it should gracefully shutdown.
      var timer = new System.Timers.Timer();
      timer.AutoReset = false;
      timer.SynchronizingObject = this;
      timer.Interval = 1000;
      timer.Elapsed = 
        (sender, args) =>
        {
          // Do a fast check to see if the worker thread is still running.
          if (WorkerThread.Join(0)) 
          {
            // Reissue the form closing event.
            Close();
          }
          else
          {
            // Keep restarting the timer until the worker thread ends.
            timer.Start();
          }
        };
      timer.Start();
    }
  }    
}

上面的代码调用了 Join,但它指定了一个超时时间为0,这会导致 Join 调用立即返回。这应该保持UI不断地传递消息。

谢谢!你的回答帮了我很多。我会按照你的建议为GUI线程制定一些待办事项序列。显然,我应该重构程序。但是现在,我的问题通过在FormClose过程中添加Join(100)来创建新线程以执行清理得到了解决=) - HtonS
并添加一个try/catch。 - HtonS

1

我建议在你的线程中设置一个顶级异常处理程序,它执行Invoke操作,并且如果发现KillThreads为true,则会忽略由Invoke引起的异常。然后,你只需要让主线程将KillThreads设置为true并允许消息循环结束即可。

我的一般技巧是使用ManualResetEvent作为其他线程中止的信号,而不是简单的布尔变量。这样做的主要原因是,如果我有长时间运行的非UI线程需要睡眠,通过使用事件,我可以将对Thread.Sleep的调用替换为对Event.WaitOne的调用,这意味着如果必须中止,则该线程将始终立即唤醒。


无论哪种情况,我通常不会等待线程发出完成信号。我相信它们会及时地这样做,并且不倾向于最终依赖于线程不运行的后续代码。如果您确实有这样的情况,也许您可以详细说明是什么?

谢谢!我发现了一个不安全的程序,其中包含Invoke(),而且没有try/catch。 - HtonS
我认为你是对的,问题在于不必要的依赖关系,我将来会采纳@Brian Gideon的建议。 - HtonS

0
你可以在所有线程上调用 Thread.Join 并通过 Thread.Abort 来终止它们,如果它们在给定的超时时间内无法加入。调用 Abort 会在线程的上下文中引发 ThreadAbortException,从而结束该线程。

Thread.Abort通常不被推荐,因为在毫不客气地从它们下面拉出地毯时,线程可能正在对全局状态进行任何操作。 - Damien_The_Unbeliever
是的,确实如此。在终止线程时必须小心谨慎。感谢您的评论。 - PVitt
正如我所写的,其中一个线程正在向外部设备发送命令,因此杀死该线程是不安全的。 - HtonS

0

我认为主要的问题(无论主线程是否等待其他线程结束)是在调用GUI线程操作之前检查KillThreads是否为false,除非你真的想在退出之前向用户显示某些内容,在这种情况下,你应该只需在主线程中循环,检查所有工作线程的ThreadState或IsAlive属性,以确定它们是否已经完成,然后退出(或在所有工作线程完成后执行所需的任何操作)。


这里有一个竞争条件 - T1 检查 KillThreads,它是 false。MainThread 将 KillThreads 设置为 true。MainThread 关闭其消息循环。T1 调用 Invoke - Damien_The_Unbeliever
你说得对。为了避免这种情况,他应该使用一些锁定机制,以防止这种竞争条件的产生。 - Hassan

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