C#: 我需要在运行时处理创建的BackgroundWorker吗?

32

通常我会在表单上使用这样的代码:

    private void PerformLongRunningOperation()
    {
        BackgroundWorker worker = new BackgroundWorker();

        worker.DoWork += delegate
        {
            // perform long running operation here
        };

        worker.RunWorkerAsync();
    }
这意味着我没有处理BackgroundWorker的释放,如果我是通过表单设计器添加它的话,它应该会被释放掉。
这样做会引起任何问题吗?声明一个模块级别的_saveWorker然后从表单的dispose方法中调用Dispose是否更正确?
8个回答

34

是的,你应该处理背景工作者。

你可能会发现使用ThreadPool.QueueUserWorkItem(...)更容易,因为它不需要任何后续清理操作。


为什么你应该总是调用Dispose()的附加详细信息:

尽管如果你查看BackgroundWorker类,它实际上并没有在其dispose方法中执行任何线程清理,但调用Dispose仍然很重要,因为该类对垃圾回收器的影响。

具有终结器的类不会立即被GC回收。它们被保留并添加到终结器队列中。然后终结器线程运行(遵循标准模式调用dispose)。这意味着对象将存活到GC第1代。而且gen 1集合比gen 0集合要少得多,因此对象在内存中存在的时间更长。

然而,如果你调用Dispose,该对象将不会被添加到终结器队列中,因此可自由进行垃圾回收。

这不是真正的大问题,但如果你正在创建许多对象,则最终会使用比必要更多的内存。通常应始终在具有dispose方法的对象上调用dispose。

所以总的来说,并不是100%的硬性要求。如果你不调用Dispose(),你的应用程序不会爆炸(甚至不会泄漏内存),但在某些情况下可能会产生负面影响。背景工作者是为作为WinForms组件使用而设计的,因此如果你有不同的要求并且不想将其用作WinForms组件,则不要使用它,要使用正确的工具来完成工作,例如ThreadPool。


8
除了常规的“因为它存在”的逻辑之外,他为什么需要调用bgw.Dispose()呢? - H H
@Henk。我添加了一些额外的细节。这不是100%关键,但它被设计成可调用的。如果您不想调用它,则有其他类适合非Winforms代码。 - Simon P Stevens

16

挑战在于确保只有在 BackgroundWorker 完成运行后才将其释放。你不能在 Completed 事件中这样做,因为该事件是由 BackgroundWorker 自身引发的。

BackgroundWorker 真正意图用于作为 WinForms 表单上的组件,因此我建议您要么这样做,要么切换到像 Thread.QueueUserWorkItem 这样的东西。这将使用一个线程池线程,并且完成后不需要任何特殊的清理。


5
赞成 BackgroundWorker 作为 WinForms 组件使用。 - Brian

7

总的来说,如果它是IDisposable,那么在使用完毕后应该Dispose()。即使当前实现的BackgroundWorker不需要被处理,你也不想被未来可能出现的内部实现所困扰。


1
我认为关于未来实现的那个点子非常好。是的,当前的BGW在其dispose方法中并没有做任何事情,但不能保证.NET 4.0的BGW不会在那里进行一些清理工作。 - Simon P Stevens
1
听起来不错,但事实上,Bgw.Dispose只是从其基类继承而来的遗留问题。 - H H
1
@henk 目前是这样的。微软不能保证将来不会发生变化,这正是为什么他们建议在实现 IDisposable 的对象上始终调用 Dispose() 的原因。 - Neil Barnwell

6

我觉得没必要担心,BackgroundWorker(后台工作者)只能持有线程(Thread),如果你的委托(delegate)中没有无限循环,那么就没有问题。

BackgroundWorker从Component继承了IDisposable(),但实际上并不需要它。

可以将其与直接将方法推送到线程池(ThreadPool)进行比较。你不能Dispose线程,尤其是来自线程池的线程。

但是,如果你的示例在使用Completed事件或Progress/Cancel特性方面是完整的,那么你也可以使用ThreadPool.QueueUserWorkItem()。


2
线程对象非常昂贵(它们至少包含大量保留的堆栈内存,可能为1MB)。 - denisenkom
是的,它们很昂贵。但你怎么清理它们?此外,Bgw使用线程池。 - H H
1
调用BGW的dispose方法至少有两个好处。首先,它会调用GC.SuppressFinalize()方法;其次,正如Wayne所说,这样做可以避免依赖内部实现细节,从而更好地适应未来的实现。 - Simon P Stevens
如果你真的担心这个问题,那么在Form.components中添加SupressFinalize可能是正确的。但我不会过分考虑future参数,因为它是从Component遗留下来的。 - H H

4
为什么不使用“using”语句呢?这样做几乎没有额外的努力,并且您可以进行处理:
private void PerformLongRunningOperation()
    {
        using (BackgroundWorker worker = new BackgroundWorker())
        {
            worker.DoWork += delegate
                             {
                                 // perform long running operation here 
                             };
            worker.RunWorkerAsync();
        }
    }

编辑:

好的,我做了一个小测试来看看处理和其他方面的情况:

using System;
using System.ComponentModel;
using System.Threading;

namespace BackgroundWorkerTest
{
    internal class Program
    {
        private static BackgroundWorker _privateWorker;

        private static void Main()
        {
            PrintThread("Main");
            _privateWorker = new BackgroundWorker();
            _privateWorker.DoWork += WorkerDoWork;
            _privateWorker.RunWorkerCompleted += WorkerRunWorkerCompleted;
            _privateWorker.Disposed += WorkerDisposed;
            _privateWorker.RunWorkerAsync();
            _privateWorker.Dispose();
            _privateWorker = null;

            using (var BW = new BackgroundWorker())
            {
                BW.DoWork += delegate
                                 {
                                     Thread.Sleep(2000);
                                     PrintThread("Using Worker Working");
                                 };
                BW.Disposed += delegate { PrintThread("Using Worker Disposed"); };
                BW.RunWorkerCompleted += delegate { PrintThread("Using Worker Completed"); };
                BW.RunWorkerAsync();
            }

            Console.ReadLine();
        }

        private static void WorkerDisposed(object sender, EventArgs e)
        {
            PrintThread("Private Worker Disposed");
        }

        private static void WorkerRunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
        {
            PrintThread("Private Worker Completed");
        }

        private static void WorkerDoWork(object sender, DoWorkEventArgs e)
        {
            Thread.Sleep(2000);
            PrintThread("Private Worker Working");
        }

        private static void PrintThread(string caller)
        {
            Console.WriteLine("{0} Thread: {1}", caller, Thread.CurrentThread.ManagedThreadId);
        }
    }
}

以下是输出结果:

Main Thread: 1
Private Worker Disposed Thread: 1
Using Worker Disposed Thread: 1
Private Worker Working Thread: 3
Using Worker Working Thread: 4
Using Worker Completed Thread: 4
Private Worker Completed Thread: 3

经过一些测试,似乎在初始化BackgroundWorker后,Dispose()对其基本上没有影响。无论您是否在using语句的范围内调用它,或者在代码中声明并立即处理和取消引用它,它仍然正常运行。Disposed事件发生在主线程上,而DoWork和RunWorkerCompleted发生在线程池线程上(在事件触发时可用的任何一个)。我尝试了一种情况,在我调用Dispose之后立即取消注册RunWorkerCompleted事件(所以在DoWork有机会完成之前),但RunWorkerCompleted没有触发。这使我相信,尽管已被处理,仍可以操作BackgroundWorker对象。
因此,正如其他人提到的那样,目前看来,调用Dispose不是真正必需的。然而,至少从我的经验和这些测试中,我没有看到任何危害。

我已经多次使用过这个方法,它确实“有效”——它不会过早地终止后台进程。然而,我从未在任何地方找到过明确的答案,是否这样做真的是正确的,或者它是否真正完成了处理。我希望有人能够确定地回答这个问题。 - Jon Comtois
我在BackgroundWorker的Disposed事件中添加了一个消息框,异步操作完成后该消息框显示出来。在我看来,BackgroundWorker在完成工作后自行处理了自己。您是否同意这种评估? - joek1975
1
我一直认为C#中的using语句是为了不需要try/finally块而方便的。因此,如果编译器内部生成了一个finally,那么在RunWorkerAsync调用之后,finally将立即被调用。 - Brett Ryan

3
在您的RunWorkerCompleted事件中调用dispose。
BackgroundWorker wkr = new BackgroundWorker();
wkr.DoWork += (s, e) => {
    // Do long running task.
};
wkr.RunWorkerCompleted += (s, e) => {
    try {
        if (e.Error != null) {
            // Handle failure.
        }
    } finally {
        // Use wkr outer instead of casting.
        wkr.Dispose();
    }
};
wkr.RunWorkerAsync();

额外的try/finally是为了确保如果您的完成代码引发异常,Dispose会被调用。

2
调用Dispose()方法被认为是一种最佳实践,适用于所有IDisposable对象。这样可以释放它们可能持有的非托管资源,例如句柄。IDisposable类还应该具有终结器,其存在可以延迟GC允许完全收集这些对象的时间。
如果您的类分配了一个IDisposable并将其分配给成员变量,则通常它本身也应该是IDisposable。
然而,如果您不调用Dispose()方法,那么最终器最终会被调用,并清理资源。显式调用Dispose()方法的优点在于,这些操作可以更快地进行,并且开销更小,从而提高应用程序性能并减少内存压力。

1
如果IDisposable类有未管理的资源需要清理,那么它们也应该有终结器,其存在可以延迟GC允许完全收集这些对象的时间。 - dss539
我建议,如果一个对象只涉及托管资源,那么通常只有在处理非托管资源时才应该实现IDisposable;否则,GC会完成所有工作(也许除了您想利用“using”语句的语义的情况)。此外,IDisposable对象经常引用其他IDisposable对象,因此它们可能不直接持有非托管资源,而是间接持有。 - RickNZ

0

完成处理程序在原始线程上运行(即不是来自线程池的后台线程)!您的测试结果实际上证实了这个前提。


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