C# IDisposable, Dispose(), lock (this)

6

我刚开始学习编程。我正在学习约翰夏普的《Microsoft Visual C#逐步学习指南(第9版)》第14章,但有些地方不太懂。

作者写道:

……finalizer(终结器)可以在对象的最后一个引用消失后任何时候运行。因此,如果Dispose方法需要进行大量工作,垃圾回收器可能会在其自己的线程上调用终结器,尤其是在Dispose方法运行时。

1) 在这里,我有一个第一个问题,这如何可能?毕竟,当没有更多链接时,GC CLR会正确地销毁对象。但如果没有引用,那么如何同时仍然可以运行一个(Dispose())方法,没有别的东西与它相关联?是的,没有链接,但方法没有完成,GC CLR将尝试删除仍在运行的方法对象吗?

此外,作者建议使用lock(this)来避免这个问题(并行调用Dispose()),并进一步说明这可能对性能产生不利影响,立即提出了本章前面描述的另一种策略。

class Example : IDisposable
{
    private Resource scarce;       // scarce resource to manage and dispose
    private bool disposed = false; // flag to indicate whether the resource
                                   // has already been disposed
    ...
    ~Example()
    {
        this.Dispose(false);
    }

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

    protected virtual void Dispose(bool disposing)
    {
        if (!this.disposed)
        {
            if (disposing)
            {
                // release large, managed resource here
                ...
            }
            // release unmanaged resources here
            ...
                this.disposed = true;
        }
    }

    //other methods
}

2) 我理解它的工作原理以及为什么需要,但是我遇到了一个问题,就是如何准确地提到 this.Dispose(true)this.Dispose(false) 的并行执行。

在提出的最终解决方案中,哪种方法将阻止GC CLR通过析构函数在其线程中与以前一样并行调用this.Dispose(false)方法,而this.Dispose(true)仍将在之前显式启动时执行?

乍一看,这类似于GC.SuppressFinalize (this) 构造,但它应该仅在结束this.Dispose(true)后才能起作用,并且在条件允许的情况下,它会长时间地在GC CLR的线程中执行,并启动析构函数和this.Dispose(false)

据我所知,没有任何防止的措施,我们只获得了取消非托管资源(文件、数据库连接等)的重复,但我们不会得到管理资源(例如大型多维数组)的重复。

这意味着可以重复释放非托管资源,却不能重复释放托管资源吗?这比使用锁定 (this) 构造更好吗?


1
如果没有引用,那么如何同时使用一个对象的方法(Dispose()),而没有其他任何引用。 - GSerg
通常情况下,如果正在调用一个实例的Dispose()方法,则this仍然是arg0参数,即使在其他任何地方都没有出现 - 因此引用确实存在。 我假设作者是在谈论某种内联调用的情况,但对于这种情况有用,我们还需要假设它在dispose期间不与任何字段交互,因为字段引用了这个需要被保留的this; 所以... 这听起来像一个极其假设的情况,实际上不能有任何可观察的副作用! - Marc Gravell
@MarcGravell “本身就是对该对象的引用。由于GC检查堆栈,因此该对象是可达的,因此:不符合收集条件。” 也就是说,GC.SuppressFinalize(this)并不期望所有先前的操作都能正常工作(即使有一千个)?它只是告诉GC在堆栈上到达该对象并检查是否需要删除时不要触碰我? - user12639686
1
@NikVladi 在执行 Dispose() 的线程中吗?它们确实是顺序执行的;然而,GC 有一个单独的线程,可以做任何它想做的事情。但是:如果您没有处理非托管数据(通常是指针):这些都不适用,您很可能会使事情过于复杂化。该死的,如果您没有非托管数据,甚至不应该拥有终结器。 - Marc Gravell
只是为了明确起见:在这里使用 lock 不是一个好主意。幸运的是,隐含的 Monitor.Exit 将与 GC.KeepAlive 执行相同的功能,并且将防止其被收集,因为如果您曾经在 GC 线程中成功地触发了 lock (Monitor.[Try]Enter),那么您已经完全破坏了应用程序(它永远无法解锁)。所以它可以工作,但原因都是错误的! - Marc Gravell
显示剩余9条评论
2个回答

5

1)我有一个问题,这怎么可能?毕竟,当没有更多的引用指向对象时,GC CLR会正确地销毁它。但是如果没有引用,那么如何同时仍然可以执行方法(Dispose())而没有其他任何指向它的东西呢?是的,没有链接,但是该方法尚未完成,GC CLR将尝试删除仍在工作的方法对象吗?

想象一下你有一个这样的方法:

void SomeMethod()
{
    var unmanagedPtr = this.MyPointer;
    while (/* some long loop */)
    {
        // lots of code that *just* uses unmanagedPtr
    }
}

现在; 这里的thisarg0,所以存在于堆栈中,但是当读取局部变量时,GC被允许查看,而arg0没有超过前几条指令被读取,所以从GC的角度来看,如果线程在while循环中,则可以忽略arg0。现在; 想象一下,对这个对象的引用只存在于arg0中-可能是因为它只是暂时存在于堆栈上,即。

new MyType(...).SomeMethod();

在这个时候,尽管一个方法正在执行,该对象仍然可以被收集。在大多数情况下,我们不会注意到任何副作用,但是:finalizers和非托管数据是一个比较特殊的情况,因为如果你的finalizer使while循环依赖的unmanagedPtr无效:就会出现一些问题。
在这里最适当的修复方法可能是在SomeMethod的末尾添加GC.KeepAlive(this)。重要的是要注意,GC.KeepAlive实际上什么都不做——它是一个不透明的、没有操作和不能内联的方法,什么都没有。通过添加GC.KeepAlive(this),我们真正做的是向arg0添加一个读取,这意味着GC需要查看arg0,以便注意到对象仍然可达,并且不会被收集。
2)在所提出的最后解决方案中,GC CLR将不允许通过析构函数并行调用this.Dispose(false)方法,就像之前一样,而this.Dispose(true)仍然会在先前显式启动时执行吗?
为了能够调用Dispose(),我们显然有一个引用,所以这很好。因此,我们知道至少在Dispose之前它是可达的,而且我们只谈论Dispose(true)与Dispose(false)竞争的情况。在这种情况下,GC.SuppressFinalize(this)有两个目的:
- GC.SuppressFinalize(this)的存在本身就像GC.KeepAlive一样标记对象为可达;在这个点之前,它肯定不会被收集。 - 一旦到达了这个点,它就不会被finalized。

1

在运行Dispose方法时,垃圾回收器可能会在其自己的线程上实际调用终结器。

(...)

这是如何可能的?

一个引用将不再使用的对象的局部变量并不会阻止垃圾回收。

请参考什么时候可以进行垃圾回收?

在该文章中,雷蒙德展示了一个对象可以在其方法执行期间变得可回收的情况。

以下代码是根据文章片段完成的:

class Color
{
    // ...
}

class SomeClass
{
    string SomeMethod(string s, bool reformulate)
    {
        OtherClass o = new OtherClass(s);
        string result = Frob(o);
        // o is elegible for garbage collection here
        if (reformulate)
        {
            Reformulate();
        }
        return result;
    }

    string Frob(OtherClass o)
    {
        string result = FrobColor(o.GetEffectiveColor());
        // o is elegible for garbage collection here
        return result;
    }

    string FrobColor(Color color)
    {
        return color.ToString();
    }

    void Reformulate()
    {
        // ...
    }
}

class OtherClass
{
    public OtherClass(string s)
    {
        _ = s;
    }

    OtherClass Parent {get; set;}

    Color Color {get; set;}

    public Color GetEffectiveColor()
    {
        Color color = this.Color;
        for (OtherClass o = this.Parent; o != null; o = o.Parent)
        {
            // this is elegible for garbage collection here
            color = BlendColors(color, o.Color);
        }
        return color;
    }

    static Color BlendColors(Color left, Color right)
    {
        _ = right;
        return left;
    }
}

请注意,垃圾收集器会扫描栈以寻找引用。因此,为了使其正常工作,需要进行内联处理。
值得注意的是,this 不是垃圾收集器或终结器线程的根。
而且这还没有谈到 WeakReference

最终器和处理方法同时运行的情况非常不可能发生。

然而,您需要决定您的类型是否是线程安全的。如果是这种情况,您应该担心两个消费者线程调用dispose。

而此模式不是线程安全的:

if (!this.disposed)
{
    // ...
    this.disposed = true;
}

我建议使用交错操作更新和检查disposed字段:

if (Interlocked.Exchange(_disposed, 1) == 0)
{
    // ...
}

然而,处理托管资源应该只调用Dispose并将其设置为null。您可以像这样操作:
Interlocked.Exchange(ref _managedResource, null)?.Dispose();

(...) 我在提到this.Dispose(true)和this.Dispose(false)的并行执行时遇到了问题。
如果终结器始终调用Dispose(false),而Dispose调用Dispose(true),则您知道条件disposing == true下的代码仅从Dispose运行。因此,它不会从终结器中并行运行。
除此之外,GC.SuppressFinalize(this)的执行会阻止终结。由于存在对this的引用,因此在调用SuppressFinalize之前对象不符合条件,而该调用完全阻止了终结。
我提醒您,finalizer的顺序是不确定的。在执行finalizer时,正在被finalize的对象的引用类型字段可能已经被finalize了。没有关于它们顺序的保证。因此,您不应该从finalizer中访问它们。
事实上,我想鼓励您不要同时处理管理和非托管资源的对象。根据单一职责原则,将任何非托管资源包装在托管资源中(通过调用某些外部API来处理它们)。 然后只有需要它们的类才需要处理托管资源(处理它们只需调用Dispose)。
此外,如果没有非托管资源,请避免使用finalizer。

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