当心使用 Task.Run 续篇

关于前两天发布的文章:为何要当心使用 Task.Run,对文中演示的示例到底会不会致使内存泄露,给不少人带来了疑惑。这点我必须向你们道歉,是我对致使内存泄漏的缘由没描述和解释清楚,也没用实际的示例证明,是个人错。函数

可是,文中示例演示的 Task.Run 捕获类成员的状况,确实会有内存泄漏的风险,我将在本文演示给你们看。测试

若是一个对象(或数据)不须要再使用了,但依然还一直占据内存空间,则视为内存泄漏。这一点你们观点是一致的吧,那如何来检测对象有没有被回收呢?code

咱们知道,在 C# 中,实例对象被释放回收,必然会执行析构函数。因此咱们能够对一个类重写其析构函数,若是该类的实例对象使用完后,强制执行 GC 回收,其析构函数依然不被执行,则说明 GC 没有回收该对象。若 GC 后面一直不回收这个对象,则说明存在内存泄漏。对象

手动强制执行 GC 回收的代码以下:blog

GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();

这三句代码能够确保 GC 把全部能搜索到的可回收对象清理干净。注意:不推荐在生产环境这样写。内存

咱们仍是用 为何要当心使用 Task.Run 这篇文章用到的示例,只是为了测试稍加修改了一下:资源

class Program
{
    static void Main(string[] args)
    {
        Test();

        // 对不须要再使用的资源强制回收
        GC.Collect();
        GC.WaitForPendingFinalizers();
        GC.Collect();

        // 程序保活
        while (true)
        {
            Thread.Sleep(100);
        }
    }

    static void Test()
    {
        var myClass = new MyClass();
        myClass.Foo();
        // 到这,myClass对象不须要再使用了
    }
}

public class MyClass
{
    private int _id;
    private List<string> _list;

    public Task Foo()
    {
        return Task.Run(() =>
        {
            Console.WriteLine($"Task.Run is executing with ID {_id}");
            Thread.Sleep(100); // 模拟耗时操做
        });
    }

    ~MyClass()
    {
        Console.WriteLine("MyClass instance has been colleted.");
    }
}

咱们在 myClass 对象使用完后,手动强制执行 GC 回收,运行结果以下:get

咱们看到 MyClass 的析构函数一直没有执行,也就意味着它的实例一直没有被回收。string

如今咱们修改 MyClass 类的 Foo 方法,改用本地(局部)变量试一试:it

...
public Task Foo()
{
    var localId = _id;
    return Task.Run(() =>
    {
        Console.WriteLine($"Task.Run is executing with ID {localId}");
    });
}
...

再运行看看效果:

此次咱们能够看到,MyClass 的析构函数执行了,说明实例对象被回收了。

先后惟一区别是,前者在 Task.Run 的匿名方法中捕获了类的成员,然后者使用了本地变量。前者出现了内存泄漏,后者避免了内存泄漏。

因此,在 Task.Run 的匿名方法中捕获类的成员,确实有可能致使内存泄漏(注意是有可能而不是必定)。

那背后的缘由是什么呢?我在上一篇文章是这样解释的:

私有成员 _idTask.Run 的匿名方法捕获使用,进而致使 MyClass 实例被引用。当外部使用完 MyClass 实例时,本该由 GC 回收的时候却发现它还被其它资源引用着,因此 GC 认为该实例不该该被回收,也就可能永远失去了被回收的机会。

这个解释有很大的问题,至少给广大读者带来了两大疑惑:

  1. 因为值类型是拷贝的方式赋值,因此捕获的本地变量和类成员指向的是各自的值,对本地变量的捕获不会影响到整个类。但若是把 _id 改成引用类型(如 String),那二者指向的就是同一个对象值,那是否是意味着即使使用本地变量也仍是没法避免内存泄漏的问题?
  2. GC 第一次回收时发现 myClass 实例存在被捕获的成员,则认为它不该该被回收。那当 Task.Run 执行完后, 被捕获的成员也使用完了,GC 再次搜索时不就能够回收 myClass 对象吗?只是晚了一些时间回收而已嘛。

感谢善于思考提出疑惑的读者们,为大家点赞。

这两大疑惑该如何解释?后半部分我还没写完,你们能够先思考一下,我将在下一篇给你们解惑,望见谅。固然,个人解释也不必定会是对的,但愿你们带着怀疑的态度和批判性思惟来看个人文章,也请你们分享本身的理解和观点。

相关文章
相关标签/搜索