关于前两天发布的文章:为什么要小心使用 Task.Run,对文中演示的示例到底会不会导致内存泄露,给很多人带来了疑惑。这点我必须向大家道歉,是我对导致内存泄漏的原因没描述和解释清楚,也没用实际的示例证实,是我的错。

但是,文中示例演示的 Task.Run 捕获类成员的情况,确实会有内存泄漏的风险,我将在本文演示给大家看。

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

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

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

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 回收,运行结果如下:

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

现在我们修改 MyClass 类的 Foo 方法,改用本地(局部)变量试一试:

...
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 对象吗?只是晚了一些时间回收而已嘛。

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

这两大疑惑该如何解释?后半部分我还没写完,大家可以先思考一下,我将在下一篇给大家解惑,望见谅。当然,我的解释也不一定会是对的,希望大家带着怀疑的态度和批判性思维来看我的文章,也请大家分享自己的理解和观点。

小心使用 Task.Run 续篇的更多相关文章

  1. 小心使用 Task.Run 解惑篇

    继上一篇文章之后,这篇文章主要解答以下两个疑惑: 由于值类型是拷贝的方式赋值,所以捕获的本地变量和类成员是指向的是各自的值,对本地变量的捕获不会影响到整个类.但如果把 _id 改为引用类型(如 Str ...

  2. 为什么要小心使用 Task.Run

    昨天在博客园有园友问了我一个问题,是这样的: 先是半个月前 @碧水青荷 童鞋的一句话"大家都说不要随便 Task.Run(()=>{}) 这样写",当时没有想太多,这句话并没 ...

  3. 对 精致码农大佬 说的 Task.Run 会存在 内存泄漏 的思考

    一:背景 1. 讲故事 这段时间项目延期,加班比较厉害,博客就稍微停了停,不过还是得持续的技术输出呀! 园子里最近挺热闹的,精致码农大佬分享了三篇文章: 为什么要小心使用 Task.Run [http ...

  4. (转).NET 4.5中使用Task.Run和Parallel.For()实现的C# Winform多线程任务及跨线程更新UI控件综合实例

    http://2sharings.com/2014/net-4-5-task-run-parallel-for-winform-cross-multiple-threads-update-ui-dem ...

  5. Task.Run Vs Task.Factory.StartNew

    在.Net 4中,Task.Factory.StartNew是启动一个新Task的首选方法.它有很多重载方法,使它在具体使用当中可以非常灵活,通过设置可选参数,可以传递任意状态,取消任务继续执行,甚至 ...

  6. Task.Run Vs Task.Factory.StartNew z

    在.Net 4中,Task.Factory.StartNew是启动一个新Task的首选方法.它有很多重载方法,使它在具体使用当中可以非常灵活,通过设置可选参数,可以传递任意状态,取消任务继续执行,甚至 ...

  7. .Net4.0如何实现.NET4.5中的Task.Run及Task.Delay方法

    前言 .NET4.0下是没有Task.Run及Task.Delay方法的,而.NET4.5已经实现,对于还在使用.NET4.0的同学来说,如何在.NET4.0下实现这两个方法呢? 在.NET4.0下, ...

  8. Task.Run与Task.Factory.StartNew的区别

    Task是可能有延迟的工作单元,目的是生成一个结果值,或产生想要的效果.任务和线程的区别是:任务代表需要执行的作业,而线程代表做这个作业的工作者. 在.Net 4中,Task.Factory.Star ...

  9. C# Task.Run 和 Task.Factory.StartNew 区别

    Task.Run 是在 dotnet framework 4.5 之后才可以使用,但是 Task.Factory.StartNew 可以使用比 Task.Run 更多的参数,可以做到更多的定制.可以认 ...

随机推荐

  1. 二:Redis:(REmote DIctionary Server)远程字典服务器

    Redis是完全开源免费的,用C语言编写的,遵循BSD协议,是一个高性能的(key-value)分布式内存数据库,基于内存运行,并支持持久化的NOSQL数据库,是当前最热门的NOSQL数据库之一,也被 ...

  2. Linxu (centos6.8)常见目录及文件解析

    /etc/sysconfig/networ-scripts/ifcfg-eth0    第一块网卡配置 BOOTPROTO="dhcp" #启用地址协议,static静态协议,bo ...

  3. 快速安装jumpserver开源堡垒机

    一 安装centos 7.X操作系统 二.选择极速安装(注意配置需要是4G2核) https://jumpserver.readthedocs.io/zh/master/install/setup_b ...

  4. Guitar Pro教程之理解记谱法

    前面的章节我们讲解了很多关于Guitar Pro'的功能使用,今天小编还是采用图文结合的方式为大家讲解它的理解记谱法,对于很多新人来说,在我们看谱之前,我们肯定要先熟悉他的一些功能如何使用以及一些关于 ...

  5. 使用pdfFactory为PDF文件设定查看选项

    一般情况下,大部分PDF文件都会按照默认的查看设置,以100%的尺寸显示第一页的内容.但在一些特殊情况下,PDF文件的创建者会设定其他的文件查看尺寸,或设定打开页为第N页,来达到引起阅读者关注的目的. ...

  6. 思维导图MindManager流程图有哪些功能

    流程图是思维导图中的一种图表,应用相当广泛.MindManager 2020作为专业的思维导图软件,更加强了流程图的功能,让用户能使用更加简便的MindManager技巧绘制流程图.接下来,就让我们一 ...

  7. 【PUPPETEER】初探之原生frame切换(四)

    一.知识点 page.frames() 使用frame.url() 获取framed的url x.getAttribute('x') 获取元素内值 二.实例 问:什么是iframe? 答:iframe ...

  8. Java基础教程——Object类

    Object类 Object类是Java所有类类型的父类(或者说祖先类更合适) <Thinking in Java(Java编程思想)>的第一章名字就叫"everything i ...

  9. 【mq读书笔记】消息到达唤醒挂起线程检查新消息

    DefaultMessageStore#start 当新消息到达CommitLog是,ReputMessageService线程负责将消息转发给ConsumeQueue,IndexFile,如果Bro ...

  10. web端项目如何测试

    1.是否支持各种网络 2.网络如果演示能否正常加载 3.加载时断网还能加载出来么 4.浏览时断网页面是否保持 5.是否兼容各种不同的浏览器 6.不同的浏览器加载出的页面是否一致 7.页面效果如何 8. ...