8

我和我的朋友讨论了 C# 中的托管和非托管资源。

据我朋友说:

1.a) C# 中的每个对象都是托管的,当我们在 C# 中编码时,没有什么像非托管对象或资源那样。非托管资源概念仅随 C++ 提供。

1.b) 无论我们在 C++ 中有托管还是非托管资源,我们都需要显式释放它。由于我们在 C# 中有自动垃圾收集器,因此我们无需考虑管理资源。

据我说:

2.a)如果我们没有非托管资源,那么为什么我们需要 C# 中的终结器或 Dispose 方法?

2.b)垃圾收集器仅具有有关已分配内存的信息,而不是有关资源状态的信息。因此我们需要在 C# 中使用 dispose 方法来释放资源。

我需要帮助来理解上述哪些参数是正确的,以及关于 c# 中非托管资源的信息,它们是否存在?

提前致谢。

4

7 回答 7

10

不,不使用非托管资源就不可能编写 C# 程序。不可避免的是,C# 程序在 100% 不受管理的操作系统上运行。如果使用文件,则使用操作系统资源。网络连接。一根线。控制台。等等,所有非常不受管理的资源。

然而,这一事实在 .NET 中隐藏得很好。框架库为这些原生对象提供了很好的包装类。文件流、套接字、线程、控制台等。内存也是一种操作系统资源,垃圾收集器是它的包装器。

在所有这些资源中,只有内存资源是真正自动管理的。他们中的其他人通过他们的包装类获得了一些帮助。它们的终结器是关键,它在调用时释放操作系统资源。这非常接近自动,垃圾收集器注意到包装类对象不再在任何地方引用,因此它释放它,终结器然后确保非托管资源也被释放。

这通常效果很好,您通常可以忽略代码中的这些实现细节。许多程序员都这样做。

但是终结器有一个问题,它们需要一段时间才能开始运行。启动它们需要进行垃圾收集,这可能需要几毫秒到几分钟不等。这是不可预测的,它取决于您在代码中消耗内存的速率。如果你不使用很多,那么它会花费很长时间。

您不能总是等待那么长时间来释放非托管资源。文件就是一个很好的例子。当您打开一个以从文件中读取数据时,您确实应该在完成读取后关闭该文件。如果您等到终结器完成该工作,那么当您需要一段时间后再次打开文件时,您将面临程序失败的风险。您可能已通过使用 FileShare.None 打开文件将自己锁定,它也会锁定您自己的代码。没什么大不了的:当你完成阅读时,你调用 Close() 来关闭文件。为了确保它被关闭,您应该将 Close() 调用放在 finally 块中,这样即使代码由于异常而中止,它也会运行。实际上,您显式地运行终结器代码。

更严重的情况是操作系统资源非常昂贵。很好的例子是位图,它们可以占用大量非托管内存或数据库连接,它们有一个池,默认情况下只包含 100 个。对于这些,您可能会让自己陷入这样一种情况,即让终结器负责释放资源只是不起作用,因为它需要太长时间。在终结器可以运行之前,您的程序因异常而死。通常很难诊断,因为这往往只在您的程序处于负载状态时发生。当很多事情发生时,在一台不在桌面上的机器上总是很难调试出现的问题。

.NET 设计者认识到了这种需求并设计了 IDisposable 接口。它的 Dispose() 方法旨在运行通常由终结器运行的代码,为您提供一种显式释放资源的方法,而不是等待垃圾收集器处理它。并且语言设计者通过将using关键字添加到他们的语言中,从而确保自动调用 IDisposable.Dispose() 来赶上潮流。

如上所述,在代码中对任何实现 IDisposable 的对象使用using或 Dispose() 是可选的,但许多 .NET 程序员认为这是至关重要的。主要是因为每个人都在没有它的情况下开始 .NET 编程,并且在他们的程序变大时迟早会遇到问题。它甚至被规定在调用 Dispose() 没有意义的类上,比如 MemoryStream。当一个类应该实现 IDisposable 但没有实现时,会导致精神上的痛苦,比如 Thread。或者当一个类同时实现 Dispose 和 Close 时(没有区别)。作为比较,Java 有相同的考虑,但没有 IDisposable。

于 2011-06-17T11:54:06.627 回答
3

在 .NET 中创建的对象是托管代码,但您的对象可以包含对非托管资源的引用。垃圾收集器 (GC) 确保在托管堆上分配的任何内存在不再需要后都会被清理。然而,虽然垃圾收集器非常擅长确保内存不会泄漏,但它对需要释放的其他资源一无所知。例如,垃圾收集器不知道如何关闭文件句柄或如何使用 CoAllocTaskMem 之类的 API 释放托管堆外分配的内存。

管理这些类型资源的对象必须确保在不再需要它们时释放它们。您可以通过覆盖 System.Object 的 Finalize 方法来完成此操作,这让垃圾收集器知道该对象想要参与自己的清理(在 C# 中,您使用 C++ 析构函数语法 ~MyObject,而不是直接覆盖该方法)。如果一个类有一个终结器,那么在收集该类型的对象之前,垃圾收集器将调用对象的终结器并允许它清理它可能持有的任何资源。

这个系统的一个问题是垃圾收集器不能确定性地运行,因此,在最后一次引用消失后,您的对象可能很长一段时间都没有最终确定。如果您的对象持有昂贵或稀有的资源,例如数据库连接,这可能是不可接受的。例如,如果您的对象仅打开了 10 个可用连接中的 1 个,则它应该尽快释放该连接,而不是等待垃圾收集器调用 finalize 方法。

为此,您应该实现一个 IDisposable 接口。阅读更多关于它的信息。

于 2011-06-17T08:42:39.433 回答
3

a) C# 中的每个对象都是托管的,当我们在 C# 中编写代码时,没有什么像非托管对象或资源那样。非托管资源概念仅随 C++ 提供。

这是不正确的。正如其他人所提到的,我们可以从外部 C#(例如 COM)获得非托管资源。

但是,在不访问非托管代码的情况下,当然可以在 C# 中拥有“非托管资源”。这些资源在垃圾回收的严格意义上可能不是不受管理的,但它们是您作为开发人员必须处理清理的资源。以一个线程为例:

class Foo
{
    private Thread thread = new Thread(new ThreadStart(DoLotsOfWork));
    private AutoResetEvent endThread = new AutoResetEvent(false);
    private int sum = 0;

    public Foo()
    {
        thread.Start();
    }

    public StopThread()
    {
        endThread.Set();
    }

    private void DoLotsOfWork()
    {
        while (!endThread.WaitOne(1000))
        {
            sum += 1;
        }
    }
}

static void Main(string[] args)
{
    Foo foo = new Foo();
    // Additional code...
    foo.StopThread();
}

假设附加代码返回或抛出异常。如果您没有显式调用 StopThread,则执行 DoLotsOfWork 的线程将不会结束,您的进程可能不会退出。

b) 无论我们在 C++ 中有托管还是非托管资源,我们都需要显式释放它。由于我们在 C# 中有自动垃圾收集器,因此我们无需考虑管理资源。

我们绝对必须考虑在 C# 中管理资源。正如您所建议的,这就是 IDisposable 存在的原因。

考虑对上述代码的这种修改:

class Foo : IDisposable
{
    private bool disposed = false;
    private Thread thread = new Thread(new ThreadStart(DoLotsOfWork));
    private AutoResetEvent endThread = new AutoResetEvent(false);
    private int sum = 0;

    public Foo()
    {
        thread.Start();
    }

    public StopThread()
    {
        endThread.Set();
    }

    public Dispose()
    {
        this.Dispose(true);
        GC.SuppressFinalize(this);
    }

    private void DoLotsOfWork()
    {
        while (!endThread.WaitOne(1000))
        {
            sum += 1;
        }
    }

    private void Dispose(bool disposing)
    {
        if (!disposed && disposing)
        {
            StopThread();
            disposed = true;
        }
    }
}

static void Main(string[] args)
{
    using (Foo foo = new Foo())
    {
        // Additional code...
    }
}

现在我们可以确定,无论附加代码做什么,由 Foo 类创建的线程都会在进程退出之前停止。

于 2011-06-17T11:25:19.497 回答
2

您在 .net 框架下创建的每件事都是托管代码,因此内存由仅由框架使用 .net 框架管理器创建的对象消耗。

在 .net 框架之外创建的每一件事都是非托管代码。

当您想要释放重物时,您可以使用终结器或处置,例如您需要关闭文件,或者您正在使用图形并且您想要释放与其关联的内存,您可以使用此方法。

于 2011-06-17T08:43:01.617 回答
1

持有非托管资源的对象会将其他实体置于某种不受欢迎的状态(例如,使它们无法用于其他目的),直到被告知不再需要它为止。如果它在没有事先被告知不再需要的情况下被放弃,那些其他实体将处于不受欢迎的状态。

托管资源是一个实体,它类似地将其他实体置于某种不受欢迎的状态,直到被告知不再需要它,但如果所有“故意”对它的引用都被放弃,它最终会自动清理自己。

来自长寿命对象的事件订阅完全存在于托管代码中,但由于它们不会在长寿命对象的生存期内自动清理,因此应将它们视为非托管资源。

于 2011-06-17T18:11:50.707 回答
1

诚然,在 CLR 中创建的所有对象都由 CLR 管理,因此您无需关心它们。

但是,当您开始使用 CLR 外部的资源(例如 COM 对象或设备上的锁)时,有责任释放这些资源。CLR 不能为你做这件事,但它提供了IDisposable让你编写代码来进行清理的接口。

于 2011-06-17T08:44:13.837 回答
1

我们确实必须处理 .NET 中的非托管资源。一个很好的例子是与数据库的连接。

我们必须明确关闭该非托管资源。这是为什么我们Dispose在 C# 中的一个示例。

于 2011-06-17T08:45:22.960 回答