后台消费者线程生命周期管理最佳实践

发布于 2024-09-16 03:47:30 字数 631 浏览 14 评论 0原文

我有一个 C# 类库,它启动一个后台消费者线程(延迟),该线程监听生产者/消费者队列中要完成的任务。该类库可以在任何类型的 .NET 应用程序中使用,并且当前在 ASP.NET MVC 网站下使用。消费者线程大部分时间都处于阻塞状态,直到请求进入队列。对于任何给定的应用程序域,应该只有一个消费者线程。

我希望能够在应用程序退出时正常关闭使用者线程,无论应用程序是什么类型,例如 Windows 窗体、控制台或 WPF 应用程序。应用程序代码本身应该不知道该线程正在徘徊等待终止这一事实。

如何从类库的角度解决这个线程生命周期问题?是否有一些全局应用程序关闭事件我可以绑定以优雅地拆除线程?

现在,由于它位于 ASP.NET MVC 应用程序域中,因此这实际上并不重要,因为无论如何它们都不会被优雅地拆除。但现在我开始在旨在终止的计划控制台应用程序任务中使用它,我想一​​劳永逸地解决这个问题。控制台应用程序不会终止,因为使用者线程仍然处于活动状态并被阻止等待请求。我在类上公开了一个公共静态 Thread 属性,以便在控制台应用程序退出时发出 Abort() 调用,但坦率地说,这是令人厌恶的。

任何指示将不胜感激!同样,我不想编写 Windows 窗体或 WPF 或控制台应用程序特定的代码来解决问题。类库的每个使用者都可以使用的通用解决方案将是最好的。

I have a C# class library which starts up a background consumer thread (lazily) which listens for tasks to complete from a producer/consumer queue. This class library can be used from any type of .NET application and is currently in use under an ASP.NET MVC web site. The consumer thread is blocked most of the time until a request comes into the queue. There should be only one consumer thread for any given app domain.

I want to be able to gracefully shut down the consumer thread when the application exits, regardless of what type of application it is, e.g. Windows Forms, Console, or WPF application. The application code itself should be ignorant of the fact that this thread is lingering around waiting to be terminated.

How can I solve this thread lifetime issue from the class library's perspective? Is there some global application shutdown event I can tie into to tear down the thread gracefully then?

Right now, since it's in an ASP.NET MVC app domain, it really doesn't matter since those never get torn down gracefully anyway. But now that I'm starting to use it in scheduled console application tasks designed to terminate, I want to solve this problem once and for all. The console application does not terminate since the consumer thread is still active and blocked waiting for requests. I've exposed a public static Thread property on the class to issue an Abort() call when the console app is exiting but this is, quite frankly, disgusting.

Any pointers would be appreciated! Again, I don't want to have to write Windows Forms or WPF or console application specific code to solve the issue. A nice generic solution that every consumer of the class library can use would be best.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

酒中人 2024-09-23 03:47:30

将线程的 IsBackground 属性设置为 true。那么它不会阻止进程结束。

http://msdn.microsoft.com/en- us/library/system.threading.thread.isbackground.aspx

或者,不使用 Abort,而是使用 Interrupt。少了很多恶心。

Set the thread's IsBackground property to true. Then it will not prevent the process from ending.

http://msdn.microsoft.com/en-us/library/system.threading.thread.isbackground.aspx

Alternately, instead of using Abort, use Interrupt. Much less disgusting.

凉世弥音 2024-09-23 03:47:30

这有点棘手。您想要避免中止是正确的,因为这可能会在重要操作(写入文件等)中间终止线程。设置 IsBackground 属性将具有相同的效果。

您需要做的就是使您的阻塞队列可取消。遗憾的是您还没有使用 .NET 4.0,否则您本可以使用 BlockingCollection 类。不过不用担心。您只需修改自定义队列,以便它定期轮询停止信号。这是一个快速而肮脏的实现,我刚刚使用阻塞队列的规范实现作为起点来创建。

public class CancellableBlockingCollection<T>
{
    private Queue<T> m_Queue = new Queue<T>();

    public T Take(WaitHandle cancel)
    {
        lock (m_Queue)
        {
            while (m_Queue.Count <= 0)
            {
                if (!Monitor.Wait(m_Queue, 1000))
                {
                    if (cancel.WaitOne(0))
                    {
                        throw new ThreadInterruptedException();
                    }
                }
            }
            return m_Queue.Dequeue();
        }
    }

    public void Add(T data)
    {
        lock (m_Queue)
        {
            m_Queue.Enqueue(data);
            Monitor.Pulse(m_Queue);
        }
    }
}

请注意 Take 方法如何接受可测试取消信号的 WaitHandle。这可能与代码其他部分中使用的等待句柄相同。这里的想法是,等待句柄在 Take 内以一定的时间间隔进行轮询,如果收到信号,则整个方法将抛出异常。假设在 Take 内部抛出是可以的,因为它处于安全点,因为消费者不可能处于重要操作的中间。此时,您可以允许线程在出现异常时解散。您的消费者线程可能如下所示。

ManualResetEvent m_Cancel = new ManualResetEvent(false);
CancellableBlockingCollection<object> m_Queue = new CancellableBlockingCollection<object>();

private void ConsumerThread()
{
  while (true)
  {
    object item = m_Queue.Take(m_Cancel); // This will throw if the event is signalled.
  }
}

您还可以通过多种其他方式取消 Take 方法。我选择使用 WaitHandle,但可以使用简单的 bool 标志轻松完成。

更新:

正如史蒂文在评论中指出的那样,Thread.Interrupt本质上已经做到了这一点。它将导致线程在阻塞调用上抛出异常...与我在本例中所追求的差不多,但代码更多。 Thread.Interrupt 的一个警告是,它仅在 .NET BCL 中的预设阻塞调用期间起作用(例如 WaitOne 等)。因此,如果您使用像本答案中那样的更加手动的方法,您将无法取消正在进行的长时间运行的计算。它绝对是放在后口袋里的好工具,并且可能在您的特定场景中有用。

This is a bit tricky. You are right on track that you want to avoid an abort as that might terminate the thread in the middle of an important operation (writing to a file, etc.). Setting the IsBackground property would have the same effect.

What you need to do is make your blocking queue cancellable. Too bad you are not using .NET 4.0 yet otherwise you could have used the BlockingCollection class. No worries though. You will just have to modify your custom queue so that it polls periodically for a stopping signal. Here is a quick and dirty implementation that I just whipped up using the canonical implementation of a blocking queue as a starting point.

public class CancellableBlockingCollection<T>
{
    private Queue<T> m_Queue = new Queue<T>();

    public T Take(WaitHandle cancel)
    {
        lock (m_Queue)
        {
            while (m_Queue.Count <= 0)
            {
                if (!Monitor.Wait(m_Queue, 1000))
                {
                    if (cancel.WaitOne(0))
                    {
                        throw new ThreadInterruptedException();
                    }
                }
            }
            return m_Queue.Dequeue();
        }
    }

    public void Add(T data)
    {
        lock (m_Queue)
        {
            m_Queue.Enqueue(data);
            Monitor.Pulse(m_Queue);
        }
    }
}

Notice how the Take method accepts a WaitHandle that can be tested for the cancel signal. This could be the same wait handle used in other parts of your code. The idea here is that the wait handle gets polled on a certain interval inside Take and if it is signaled then the whole method throws. The assumption is that throwing inside Take is okay since it is at a safe point because the consumer could not have been in the middle of an important operation. You can just allow the thread to disintegrate upon the exception at this point. Here is what your consumer thread may look like.

ManualResetEvent m_Cancel = new ManualResetEvent(false);
CancellableBlockingCollection<object> m_Queue = new CancellableBlockingCollection<object>();

private void ConsumerThread()
{
  while (true)
  {
    object item = m_Queue.Take(m_Cancel); // This will throw if the event is signalled.
  }
}

There are various other ways you could cancel the Take method. I chose to use a WaitHandle, but it could easily be done using a simple bool flag for example.

Update:

As Steven pointed out in the comments Thread.Interrupt does essentially this already. It will cause a thread to throw an exception on a blocking call...pretty much what I was after in this example, but with a lot more code. One caveat with Thread.Interrupt is that it only works during the canned blocking calls in the .NET BCL (like WaitOne, etc.). So you would not be able to cancel a long running computation in progress like you would if you used a more manual approach like the one in this answer. It is definitely great tool to keep in your back pocket and might just be useful in your specific scenario.

世界和平 2024-09-23 03:47:30

如果您使用 .net 4.0 CancelationToken可用于向线程发出信号,表明该线程正常关闭了。它非常有用,因为大多数 Wait 命令允许您向其传递一个令牌,并且如果线程在取消时处于等待状态,它将自动退出等待。

If you are using .net 4.0 a CancelationToken can be used to signal the thread that it is time to gracefully shut down. It is very useful as most Wait commands allow you to pass it a token and if the thread is in a wait when the thread is canceled it will automatically come out of the wait.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文