停止一个线程,ManualResetEvent,volatile boolean或cancellationToken

8

我有一个在Windows服务中执行大量工作的线程(STAThread)。当Windows服务重新启动时,我希望能够优雅地停止这个线程。

我知道几种方法:

  • 一个易失性布尔值
  • ManualResetEvent
  • CancellationToken

据我所知,Thread.Abort不可行...

最佳实践是什么? 工作是在另一个类中完成的,而不是启动线程的类,因此必须在构造函数中引入cancellationToken参数,或者例如使用易失性变量。但我就是想不明白什么是最聪明的做法。

更新
只是为了澄清一下,我已经包装了一个非常简单的示例来说明我的意思。如前所述,这是在Windows服务中完成的。现在我正在考虑在循环中检查的易失性布尔值或CancellationToken... 我不能等待循环完成,因为如下所述,可能需要几分钟时间,使服务器的系统管理员认为服务出了问题而需要重新启动它... 我可以轻松地放弃循环中的所有工作,但我不能使用Thread.Abort来做到这一点,它是“邪恶”的,而且还需要调用COM接口,因此需要进行一些小的清理。

Class Scheduler{
  private Thread apartmentThread;
  private Worker worker;

  void Scheduling(){
    worker = new Worker();
    apartmentThread = new Thread(Run);
    apartmentThread.SetApartmentState(ApartmentState.STA);
    apartmentThread.Start();    
  }

  private void Run() {
    while (!token.IsCancellationRequested) {
      Thread.Sleep(pollInterval * MillisecondsToSeconds);
      if (!token.IsCancellationRequested) {
        worker.DoWork();
      }
    }
  }
}

Class Worker{
  //This will take several minutes....
  public void DoWork(){
    for(int i = 0; i < 50000; i++){
      //Do some work including communication with a COM interface
      //Communication with COM interface doesn't take long
    }
  }
}

更新
刚刚测试了性能,使用一个cancellationToken,在代码中检查isCancelled状态要比在ManualResetEventSlim上使用waitOne快得多。一些快速的数字,如果在for循环中迭代100,000,000次的cancellationToken花费我大约500毫秒,而WaitOne花费大约3秒。因此,在这种情况下,使用cancellationToken可以提高性能。


2
请查看有关重置事件和volatile的相关问题:https://dev59.com/f2ct5IYBdhLWcg3wnuvg 对于您的情况,我个人通常使用ManualResetEvent,因为它是系统专门为此类任务提供的。 - Maxim Zabolotskikh
4个回答

5

使用WaitHandle,最好是ManualResetEvent。你最好让循环中的任何操作都完成。这是实现目标的最安全方法。

ManualResetEvent _stopSignal = new ManualResetEvent(false); // Your "stopper"
ManualResetEvent _exitedSignal = new ManualResetEvent(false);

void DoProcessing() {
    try {
        while (!_stopSignal.WaitOne(0)) {
            DoSomething();
        }
    }
    finally {
        _exitedSignal.Set();
    }
}

void DoSomething() {
    //Some work goes here
}

public void Terminate() {
    _stopSignal.Set();
    _exitedSignal.WaitOne();
}

然后使用它:

Thread thread = new Thread(() => { thing.DoProcessing(); });
thread.Start();

//Some time later...
thing.Terminate();

如果您的“DoSomething”实现中有一个特别长时间运行的进程,您可能希望异步调用它,并为其提供状态信息。虽然这可能会变得非常复杂,但最好等待您的进程完成,然后退出(如果您能够)。


这可能是一种不错的方法,但是我的执行DoProcessing的线程是STAThread,这会有问题吗? - dennis_ler
这应该不是问题。(至少我运行时没有出现过) - Sean H

5
你没有发布足够的实现细节,但如果你有CancellationToken,我强烈建议你使用它。从可维护性的角度来看,它非常简单易懂。如果你决定有多个工作线程,你还可以设置协作取消。
如果您发现自己处于可能长时间阻塞的情况下,最好设置您的架构,使其不会发生这种情况。当你告诉它们停止时,你不应该启动不友好的线程。如果他们不听从你的要求停止,唯一真正的方法就是关闭进程并让操作系统杀死它们。
Eric Lippert在这里回答了一个相关问题,非常棒。

如我最新的更新中所提到的,我发现在性能方面最好使用CancellationToken... - dennis_ler

4

我倾向于使用一个布尔标志、一个锁对象和一个Terminate()方法,例如:

object locker = new object();
bool do_term = false;

Thread thread = new Thread(ThreadStart(ThreadProc));
thread.Start();

void ThreadProc()
{
    while (true) {
        lock (locker) {
            if (do_term) break;
        }

        ... do work...
    }
}

void Terminate()
{
    lock (locker) {
        do_term = true;
    }
}

除了Terminate()之外,“worker”类的所有其他字段和方法都是私有的。


1
这是真的,它将否定锁的需要。 - Lloyd
我只是想知道为什么你使用锁定语句?是因为你的示例是在许多线程运行相同代码的常见方式吗? - witoong623
@witoong623 因为两个不同的线程访问同一个变量。 - Lloyd
@Lloyd 我不明白...我对线程很新,我的理解是不同的线程在相同的代码上运行,但不在相同的上下文中(不确定上下文是什么?类似于另一个实例),因此不需要锁定语句。我有误解吗? - witoong623
两个线程可以同时执行,这意味着变量的状态可以在不同线程之间的读取和写入之间发生改变。 - Lloyd

3

有两种情况可能会出现线程问题:

  • 正在处理。
  • 阻塞。

如果您的线程正在处理某些东西,那么您必须等待线程完成处理才能安全地退出。如果它是工作循环的一部分,那么可以使用布尔标志来终止循环。

如果您的线程被阻塞,那么您需要唤醒线程并让其再次开始处理。线程可能会在ManualResetEvent、数据库调用、套接字调用或其他任何可能阻塞的操作上被阻塞。为了唤醒它,您必须调用Thread.Interrupt()方法,这将引发ThreadInterruptedException异常。

可能看起来像这样:

private object sync = new object():
private bool running = false;

private void Run()
{
    running = true;
    while(true)
    {
        try
        {
            lock(sync)
            {
                if(!running)
                {
                    break;
                }
            }

            BlockingFunction();
        }
        catch(ThreadInterruptedException)
        {
            break;
        }
    }
}

public void Stop()
{
    lock(sync)
    {
        running = false;
    }
}

以下是如何使用它的方法:

MyRunner r = new MyRunner();
Thread t = new Thread(()=>
{
    r.Run();
});

t.IsBackground = true;
t.Start();

// To stop the thread
r.Stop();

// Interrupt the thread if it's in a blocking state
t.Interrupt();

// Wait for the thread to exit
t.Join();

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