当前位置:首页 > 技术 > 正文内容

小心使用 Task.Run 解惑篇

fengm2个月前 (12-09)技术184

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

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

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

public class MyClass
{
    private int _id;

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

先来看第一个疑惑。经实测,把 _id 改为 StringBuilder 类型运行结果是和 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内存泄漏的争议》

扫描二维码至手机访问

扫描二维码推送至手机访问。

版权声明:本文由风芒博客新闻信息分享笔录网站发布,如需转载请注明出处。

转载请注明出处:http://fengm.top/713.html

分享给朋友:

相关文章

Redis Sentinel-深入浅出原理和实战

Redis Sentinel-深入浅出原理和实战

本篇博客会简单的介绍Redis的Sentinel相关的原理,同时也会在最后的文章给出硬核的实战教程,让你在了解原理之后,能够实际上手的体验整个过程。 之前的文章聊到了Redis的主从复制,...

缓存穿透、击穿、雪崩什么的傻傻分不清楚?看了这篇文后,我明白了

缓存穿透、击穿、雪崩什么的傻傻分不清楚?看了这篇文后,我明白了

对于缓存,大家肯定都不陌生,不管是前端还是服务端开发,缓存几乎都是必不可少的优化方式之一。在实际生产环境中,缓存的使用规范也是一直备受重视的,如果使用的不好,很容易就遇到缓存击穿、雪崩...

MYSQL学习(二) --MYSQL框架

MYSQL学习(二) --MYSQL框架

MYSQL架构理解 通过对MYSQL重要的几个属性的理解,建立一个基本的MYSQL的知识框架。后续再补充完善。 一、MYSQL架构   这里给的架构描述,是很宏观的架构。有助于建立对MYSQL整...

JAVA_数据类型介绍与基本数据类型之间的运算规则

JAVA_数据类型介绍与基本数据类型之间的运算规则

基本数据类型 整型: byte、short、int、long java 的整型常量默认为int型,在java程序中变量通常声明为int型,除非不足以表示较大的数才用long,而在声明long...

ASP.NET Core管道详解[2]: HttpContext本质论

ASP.NET Core管道详解[2]: HttpContext本质论

ASP.NET Core请求处理管道由一个服务器和一组有序排列的中间件构成,所有中间件针对请求的处理都在通过HttpContext对象表示的上下文中进行。由于应用程序总是利用服务器来完成对请求的接收和...

分享:

支付宝

微信