上一篇文章之后,这篇文章主要解答以下两个疑惑:

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

为了方便理解,我再把昨天的关键代码贴出来:

public class MyClass
{
private int _id;
private List<string> _list; public Task Foo()
{
var localId = _id;
return Task.Run(() =>
{
Console.WriteLine($"Task.Run is executing with ID {localId}");
Thread.Sleep(100); // 模拟耗时操作
});
}
}

先来看第一个疑惑。经实测,把 _id 改为 String 类型运行结果是和 int 一样的,说明和值类型或引用类型无关。我的理解是这样的:

我们知道,引用类型的变量在声明的时候就会在栈中分配一个空间,用来存放地址引用,而给它的赋值则存储在托管堆中。虽然本地变量 localId 和类的成员 _id 的地址都指向的是托管堆中同一块空间,但他们在栈中的地址却分属不同的作用域。所谓被捕获就是被作用域捕获,当一个作用域结束时,该作用域内的成员的地址空间都会随着一起被释放。至于地址指向的托管堆中的字符串值,则不是作用域关心的事情。当该字符串值所在的空间没有地址指向它时,就会被 GC 回收。 有点抽象,但应该还好理解。

再来看第二个疑惑。在此之前,我们先来了解一下 GC 的分代算法。

当 CLR 试图搜索不再使用的对象的时,它需要遍历托管堆上的对象。随着程序的持续运行,托管堆可能越来越大,如果要对整个托管堆进行垃圾回收,势必会严重影响性能。所以,为了优化这个过程,CLR 中使用了分代算法

简单来说,分代算法就是把内存中的资源划分为三代:Gen 0、Gen 1、Gen 2,它们被 GC 遍历的频率依次从高到低。所有新创建的对象属于 Gen 0,GC 扫描它的频率最高。进行一次扫描后,处于 Gen 0 的不可回收对象就会被标记为 Gen 1。类似的,GC 扫描 Gen 1 时,如果 Gen 1 的对象依然不可回收,就会标记为 Gen 2。有点像马太效应,资源停留在内存时间越长,就越不容易被回收。

Gen 2 的回收被称为 Full GC。而 Full GC 只有在满足一定的条件才会执行,具体请阅读这篇官方文档:

https://docs.microsoft.com/en-us/dotnet/standard/garbage-collection/notifications#full-garbage-collection

也就是说,进入 Gen 2 的资源,若条件没有达到,就会一直不被回收。

理解了分代算法和 Full GC,第二个疑惑就迎刃而解了。第二个疑惑关键在三个时间点上:

  1. myClass 对象作用域结束的时间点
  2. GC 执行回收的时间点
  3. Task.Run 匿名方法执行完成的时间点

如果程序执行的时间点顺序是:1、3、2,那么不会有内存漏泄的问题,这点很容易理解。

由于实际情况 Task.Run 一般为耗时操作(非耗时任务一般没有必要使用 Task.Run),所以时间点的顺序极有可能是:1、2、3。如果是此执行的顺序,那么 GC 在回收时就会因为 myClass 对象存在成员被引用而把它标记为 Gen 1。如果 Task.Run 耗时足够长, myClass 就可能会进入 Gen 2,进而可能很难被回收,甚至可能永远不被回收。

其实大部分场景,我们也不必过于小心,即使在 Task.Run 匿名方法捕获了类的成员使该类的实例进入了 Gen 2,Gen 2 中留存的不再使用的资源也是有限的。根据官方文档对 Full GC 的介绍(地址在前文),当 Gen 2 积累到一定的量时便满足了执行回收的条件,在 GC 下一次回收时便会回收 Gen 2 中不再使用的资源。当然,作为一个优秀的程序员,我们还是得养成好的编码习惯,不要在 Task.Run 中的匿名方法捕获类的成员。

最后,郑重声明,最近三篇关于小心使用 Task.Run 的文章皆属我个人理解,知识水平有限,难免存在遗漏和错误。若有发现,请大家不吝指正。

PS:本人博客园文章一般晚于公众号一天发布,望大家见谅。关于是否属于内存泄漏问题,我在今天的文章中有讨论:《.NET内存泄漏的争议》

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

  1. 小心使用 Task.Run 续篇

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

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

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

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

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

  4. 【突然想多了解一点】可以用 Task.Run() 将同步方法包装为异步方法吗?

    [突然想多了解一点]可以用 Task.Run() 将同步方法包装为异步方法吗? 本文翻译自<Should I expose asynchronous wrappers for synchrono ...

  5. (转).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 ...

  6. Task.Run Vs Task.Factory.StartNew

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

  7. Task.Run Vs Task.Factory.StartNew z

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

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

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

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

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

随机推荐

  1. Spring笔记(8) - @EventListener注解探究

    在上文中讲了Spring的事件监听机制,流程是:定义事件.监听器,发布事件,控制台输出监听到的事件内容. 在上文的扩展中 使用 @EventListener 注解来自定义监听器,监听指定的事件,比如下 ...

  2. 记在Linux上定位后台服务偶发崩溃的问题

    问题描述 在最近的后台服务中,新增将某个指令的请求数据落盘保存的功能.在具体实现时,采用成员变量来保存请求消息代理头,在接收响应以及消息管理类释放时进行销毁.测试反馈,该服务偶发崩溃. 问题分析 测试 ...

  3. python3中我所了解的print()的用法

    1.最基础的用法:打印调试信息等字符串语句.而且在3里面,打印中文的时候不需要加u了. 2.打印变量 打印默认换行的: 打印出来不想要他换行的:参数end='',这样打印出来就可以不换行了,这种骚操作 ...

  4. linux中ugo权限管理(chmod/chown)

    查看ugo权限: ll  [root@localhost test]# ll total 12 -rwxr-xr-x 2 root root 4 Oct  3 11:44 a lrwxrwxrwx 1 ...

  5. Docker学习6:使用docker构建Jekyll服务和java服务

    写在前面 ## 文章Dockerfile中涉及apt-get 等操作需更换镜像 在Dockerfile中添加下列 Dockerfile源码,见下面作者githubhttps://github.com/ ...

  6. kali 系列学习10-渗透攻击MySQL数据库服务、PostgreSQL数据库服务、Tomcat服务和PDF文件

    介绍使用MSFCONSOLE工具渗透攻击MySQL数据库服务.PostgreSQL数据库服务.Tomcat服务和PDF文件等. 6.4.1 渗透攻击MySQL数据库服务 MySQL是一个关系型数据库管 ...

  7. YARN-MapReduce的作业提交流程

    YARN分布式资源管理系统 组成: ResourceManager:YARN的资源管理器,主节点,通过NodeManager管理集群中所有的资源 NodeManager:YARN的节点管理器,从节点, ...

  8. 修改pycharm中的flask项目名遇到的坑

    曾修改过自己的项目名,并在settings中的解释器也更正过来了.然后执行pip list 报错: failed to create process. 解决方法如下: 到你的项目的venv目录下的Sc ...

  9. Guitar Pro的10个非常实用的技巧(下)

    Guitar Pro 7具有许多功能和编辑选项,只需点击几下即可随时创建与编辑我们的乐谱,.以下就为大家介绍10个Guitar Pro中实用的技巧,可以大大的节省我们的时间. 上次在<Guita ...

  10. CorelDRAW软件的出血位详解

    出血位,一种常见的印刷术语,在印刷品中,设计的图形一般比成品尺寸要大,会加一些延伸,专门用来给生产工艺中的公差范围使用,以避免最后裁切的成品中有白边或是裁到内容.多出来的部分在印刷后要被裁掉,这部分就 ...