如何在C#中正确终止工作线程

7

问题陈述

我有一个工作线程,主要是扫描一个文件夹,进入其中的文件,然后休眠一段时间。扫描操作可能需要2-3秒钟,但不会更长。我正在寻找一种优雅地停止这个线程的方法。

澄清:我想在线程处于休眠状态时停止它,而不是在线程扫描时停止它。然而,问题在于我不知道线程的当前状态。如果它正在休眠,我希望它立即退出。如果它正在扫描,则希望在它尝试阻塞的那一刻退出。

解决方案尝试

起初我使用了Sleep和Interrupt。然后我发现Interrupt并不能真正中断Sleep - 它只在线程试图进入休眠时起作用。

所以我切换到了Monitor Wait&Pulse。然后我发现Pulse只在我实际处于Wait状态时起作用。所以现在我有一个线程,看起来像这样:

while (m_shouldRun)
{
    try
    {
        DoSomethingThatTakesSeveralSeconds();
        lock (this)
        {
            Monitor.Wait(this, 5000);
        }
    }
    catch (ThreadInterruptedException)
    {
        m_shouldRun = false;
    }
}

现在我需要编写停止函数。因此,我从以下内容开始:

public void Stop()
{
    m_shouldRun = false;
    lock (this)
    {
        Monitor.Pulse(this);
    }
    thread.Join();
}

但这个方法不起作用,因为我可能在线程工作时(而它没有等待)进行脉冲。所以我添加了中断:

public void Stop()
{
    m_shouldRun = false;
    thread.Interrupt();
    lock (this)
    {
        Monitor.Pulse(this);
    }
    thread.Join();
}

另一个选择是使用:
public void Stop()
{
    m_shouldRun = false;
    while (!thread.Join(1000))
    {
        lock (this)
        {
            Monitor.Pulse(this);
        }
    }
}

问题

哪种方法是首选的?是否有第三种更可取的方法?


“起初我使用了Sleep和Interrupt。然后我发现Interrupt并不真正打断Sleep - 它只在线程试图进入睡眠时才起作用。” 这很奇怪。这是已知的bug吗?如果不是,你可以尝试使用一个玩具示例来重现它吗? - Heinzi
4个回答

10

优雅地停止线程的方法是让它自己完成。因此,在工作方法中,您可以有一个布尔变量来检查是否要中断。默认情况下,它将被设置为false,当您从主线程将其设置为true时,它将通过从处理循环中退出来简单地停止扫描操作。


3
+1 表示允许线程自行完成,其他方式都会很混乱。不要忘记在布尔标志上标记 volatile 关键字。 - spender
1
谢谢。您会注意到我有这样一个标志。当线程正在工作时,我不会中断它,但是当它在睡眠时,我想要中断它。如果它要睡10分钟,我不希望它继续睡觉。 - Eldad Mor
2
你会如何建议我从线程池中取得一个实例并每隔10分钟运行一次扫描:使用ThreadPool.RegisterWaitForSingleObject方法,并将executeOnlyOnce参数设置为false。当你决定停止时,只需使用RegisteredWaitHandle.Unregister取消注册等待句柄即可。 - Darin Dimitrov
有趣。让我们看看我是否理解正确。从我的主线程中,我调用RegisterWaitForSingleObject,设置10分钟的超时时间,并将“DoSomething”函数作为回调函数。我从未发出过等待对象的信号,因此触发器只会在10分钟后发生。一旦回调完成 - 等待将重新开始。这不仅是将“线程”委托给.NET吗?我的意思是 - 等待线程无论如何都会存在,对吧? - Eldad Mor
@Regent,那不是一个忙等待吗(即使它大部分时间都在睡眠)?如果我想让线程“立即”退出,我需要在该方法下将间隔分成10毫秒的部分。 - Eldad Mor
显示剩余3条评论

10

另一种选择是使用事件:

private ManualResetEvent _event = new ManualResetEvent(false);


public void Run() 
{
 while (true)
 {
    DoSomethingThatTakesSeveralSeconds();
    if (_event.WaitOne(timeout))
      break;
 }
}

public void Stop() 
{
   _event.Set();
   thread.Join();
}

是的,这也可以工作。问题是最好的方法是什么。您的选项看起来比重复脉冲或中断+脉冲更好。 - Eldad Mor
1
个人而言,我不会使用Pulse/Interrupts。由于可能存在APC(我认为线程可能未注意到),Pulse可能存在问题。中断的声誉不佳:http://www.bluebytesoftware.com/blog/2007/08/23/ThreadInterruptsAreAlmostAsEvilAsThreadAborts.aspx - liggett78
如果只有一个线程执行任务,请使用事件(event)。否则,可以使用一个易变标志(volatile flag)和超时来唤醒和检查标志是否被设置(避免创建大量内核对象事件)。 - liggett78

1

我建议保持简单:

while (m_shouldRun)
{
    DoSomethingThatTakesSeveralSeconds();
    for (int i = 0; i < 5; i++)  // example: 5 seconds sleep
    {
        if (!m_shouldRun)
            break;
        Thread.Sleep(1000);
    }
}

public void Stop()
{
    m_shouldRun = false;
    // maybe thread.Join();
}

这样做有以下优点:

  • 它看起来像繁忙等待,但实际上不是。在等待阶段进行了 $NUMBER_OF_SECONDS 次检查,这与真正的繁忙等待进行的数千次检查不可比较。
  • 它很简单,大大降低了多线程代码出错的风险。你的 Stop 方法只需要将 m_shouldRun 设置为 false,然后(如果必要)调用 Thread.Join(如果需要在离开 Stop 之前完成线程)。不需要同步原语(除了将 m_shouldRun 标记为 volatile)。

DoSomething函数不会被强制中断。Thread.Interrupt只有在线程尝试阻塞时才会“发生”。请参阅MS文档(此处:http://msdn.microsoft.com/en-us/library/system.threading.thread.interrupt.aspx)-“如果此线程当前未在等待、睡眠或加入状态下阻塞,则在下次开始阻塞时将中断它。” - Eldad Mor
@Eldad:好观点,我把它和Thread.Abort混淆了。我改变了我的答案。 - Heinzi

0

我想到了分别安排任务:

using System;
using System.Threading;

namespace ProjectEuler
{
    class Program
    {
        //const double cycleIntervalMilliseconds = 10 * 60 * 1000;
        const double cycleIntervalMilliseconds = 5 * 1000;
        static readonly System.Timers.Timer scanTimer =
            new System.Timers.Timer(cycleIntervalMilliseconds);
        static bool scanningEnabled = true;
        static readonly ManualResetEvent scanFinished =
            new ManualResetEvent(true);

        static void Main(string[] args)
        {
            scanTimer.Elapsed +=
                new System.Timers.ElapsedEventHandler(scanTimer_Elapsed);
            scanTimer.Enabled = true;

            Console.ReadLine();
            scanningEnabled = false;
            scanFinished.WaitOne();
        }

        static void  scanTimer_Elapsed(object sender,
            System.Timers.ElapsedEventArgs e)
        {
            scanFinished.Reset();
            scanTimer.Enabled = false;

            if (scanningEnabled)
            {
                try
                {
                    Console.WriteLine("Processing");
                    Thread.Sleep(5000);
                    Console.WriteLine("Finished");
                }
                finally
                {
                    scanTimer.Enabled = scanningEnabled;
                    scanFinished.Set();
                }
            }
        }
    }
}

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