本随笔续接:.NET 同步与异步 之 原子操作和自旋锁(Interlocked、SpinLock)(九)

至此、同步与异步 相关的常规操作(比较常见的操作)、差不多已经介绍完毕。 本随笔就着重说一下闭包、因闭包可能会导致一些意想不到的的bug。

(PS:至于 WaitHandle家族相关随笔、最后补充)

一、警惕闭包

            int total = ;

            List<Task> taskList = new List<Task>();

            for (int i = ; i < ; i++)
{
var task = Task.Run(() =>
{
System.Threading.Interlocked.Add(ref total, i);
}); taskList.Add(task);
} Task.WaitAll(taskList.ToArray()); PrintInfo(total.ToString()); // 输出结果

这个demo逻辑很简单、在循环中异步累加循环变量i的值,当然所有异步操作完成后,输出累加结果。 1+2+3 ... + 9 结果应该是 45.

如果你看到这里没有什么问题、也没发现什么问题。那么你可能掉坑里了、这是并发中比较常见的一类问题、也是本随笔的要着重说明的一点:警惕闭包。

其实这里的输出结果是随机的、应该在 [45 ~ 100] 之间。 看到这里,如果你能想明白为什么,那么本篇随笔的一半内容已经明白了。

二、闭包的本质

从本质上说,闭包是一段可执行的代码块,但是这段代码块额外维护了一块上下文环境(内存),即使上下文环境中的某个局部变量、已经超出了其原本所在的代码块的作用域,闭包也依然可以对其进行访问

        /// <summary>
/// 窥探闭包的本质
/// </summary>
public void Demo2()
{
var func = GetFunc(); PrintInfo($"result:{func().ToString()}"); // 输出结果 结果为 12
} private Func<int> GetFunc()
{
int result = ; Func<int> func = () =>
{
result++; return result;
}; result++; return func;
}

在上面例子里的Demo中,局部变量result 的作用域是 GetFunc 方法,但是当 GetFunc 方法执行完毕后,在 Demo2 方法中 调用 匿名委托 Func<int> 时,依然可以访问  result 变量,这就是闭包。

三、一探究竟

让我们来借助IL来一探究竟、看看这个过程中究竟发生了什么、首先看一下 GetFunc 这个方法,顺带这个机会、较为深入的介绍一下IL:

.method private hidebysig instance class [mscorlib]System.Func`<int32>
GetFunc() cil managed
{
// 标示分配的堆栈的大小、在该方法执行过程中、堆栈中最多可同时存放3个数据项
// 代码大小 50 (0x32)
.maxstack 3
// 声明该方法中需要的四个局部变量,形如: [索引] 类型 名称
// 索引为 0 的类型,是编译器自动生成的类型、该类型有两个成员、一个int result、一个 返回值为 int类型的方法,具体详情稍后再介绍。
.locals init ([] class ParallelDemo.Demo.VariableCapturingClass/'<>c__DisplayClass3_0' 'CS$<>8__locals0',
[] class [mscorlib]System.Func`<int32> func,
[] int32 V_2,
[] class [mscorlib]System.Func`<int32> V_3)
// new 一个对象、其类型为编译器自动生成的类型、新 new出来的对象的引用 将被push到堆栈上
IL_0000: newobj instance void ParallelDemo.Demo.VariableCapturingClass/'<>c__DisplayClass3_0'::.ctor()
// POP堆栈, 并将POP出的数据赋值给 索引为0的局部变量。 此时堆栈中已经没有数据了
IL_0005: stloc.0
// 无意义的操作
IL_0006: nop // 将索引为0的局部变量push到堆栈
IL_0007: ldloc.0
// 将整形数字 10 push到堆栈
IL_0008: ldc.i4.s 10
// POP 堆栈中的两个数据, 并将第二个数据赋值给 第一个数据(引用)的result字段。 此时堆栈中已经没有数据了 【完成了 result = 10 的赋值操作】
IL_000a: stfld int32 ParallelDemo.Demo.VariableCapturingClass/'<>c__DisplayClass3_0'::result // 将索引为0的局部变量push到堆栈
IL_000f: ldloc.0
// 将其方法所对应的非托管代码指针 push到堆栈上
IL_0010: ldftn instance int32 ParallelDemo.Demo.VariableCapturingClass/'<>c__DisplayClass3_0'::'<GetFunc>b__0'()
// POP 构造函数所需要的两个参数、 并 new 一个 Func<int> 类型委托, 并将新对象引用push到堆栈
IL_0016: newobj instance void class [mscorlib]System.Func`<int32>::.ctor(object,
native int)
// POP堆栈, 并将值赋值给索引为1的局部变量 此时堆栈中已经没有数据了 【完成了 new Func<int> 的操作】
IL_001b: stloc.1 // 将索引为0的局部变量push到堆栈
IL_001c: ldloc.0
// POP堆栈, 并将POP出的数据(引用)的result字段值 push到堆栈
IL_001d: ldfld int32 ParallelDemo.Demo.VariableCapturingClass/'<>c__DisplayClass3_0'::result
// POP堆栈, 并将POP出的数据赋值给 索引为 2的局部变量 此时堆栈中已经没有数据了
IL_0022: stloc.2
// 将索引为0的局部变量push到堆栈
IL_0023: ldloc.0
// 将索引为2的局部变量push到堆栈
IL_0024: ldloc.2
// 将 数字1push到堆栈
IL_0025: ldc.i4.1
// POP两个数据 并求和, 并将结果push到堆栈
IL_0026: add
// POP两个数据, 并将第二个数据的值赋值给 第一个数据(引用)的result字段 此时堆栈中已经没有数据了 【完成了 result++ 的操作】
IL_0027: stfld int32 ParallelDemo.Demo.VariableCapturingClass/'<>c__DisplayClass3_0'::result // 将索引为1的局部变量push到堆栈
IL_002c: ldloc.1
// POP堆栈, 并将POP出的数据赋值给 索引为3的局部变量 此时堆栈数据为空 【完成返回值的准备工具】
IL_002d: stloc.3 // 跳转代码至 IL_0030
IL_002e: br.s IL_0030
// push索引为3的局部变量到堆栈
IL_0030: ldloc.3
// 返回 return
IL_0031: ret
} // end of method VariableCapturingClass::GetFunc

看完上面的IL的代码解释,你可能唯一不太明白的就是 编译器自动生成的类型,那接下来,我们将看一看 这个自动生成的类型,到底是什么东西:

根据上图,我们可以确定:

1、新生成的类型是一个class

2、含有 result字段 ,类型为int32.

3、含有一个 <GetFunc>b__0的方法, 返回值 为 int32类型

让我们在看看 <GetFunc>b__0 这个方法的IL代码:

.method assembly hidebysig instance int32
'<GetFunc>b__0'() cil managed
{
// 代码大小 28 (0x1c)
.maxstack
.locals init ([] int32 V_0,
[] int32 V_1)
IL_0000: nop
// push this引用到堆栈
IL_0001: ldarg.0
// POP堆栈, 并将 POP出的数据(引用)的result字段值 push到堆栈 其他IL代码就不一一解释了, 因为前文都已经介绍过了。
IL_0002: ldfld int32 ParallelDemo.Demo.VariableCapturingClass/'<>c__DisplayClass3_0'::result
IL_0007: stloc.0
IL_0008: ldarg.0
IL_0009: ldloc.0
IL_000a: ldc.i4.1
IL_000b: add
IL_000c: stfld int32 ParallelDemo.Demo.VariableCapturingClass/'<>c__DisplayClass3_0'::result
IL_0011: ldarg.0
IL_0012: ldfld int32 ParallelDemo.Demo.VariableCapturingClass/'<>c__DisplayClass3_0'::result
IL_0017: stloc.1
IL_0018: br.s IL_001a
IL_001a: ldloc.1
IL_001b: ret
} // end of method '<>c__DisplayClass3_0'::'<GetFunc>b__0'

看完IL代码,你会发现 <GetFunc>b__0 这个方法实际就是匿名委托Func<int> 所指向的方法。 而 闭包所维护的上下文环境 其实就是 result 字段。

四、回顾

最后、我们再回头看一下第一个Demo:警惕闭包。

在这个demo中、循环变量 i 是闭包中的上下文环境之一(total也是),由于累加是在Task任务中进行的,Task任务什么时候被执行是由调度器和线程池两个因素决定的,并且task任务被执行的时间点往往会略有延迟,因此 循环变量 i的值 会被累加的过大,因此结果会偏大,所以结果是一个随机数 [45 ~ 100] .

随笔暂告一段落、下一篇随笔:  线程安全的集合(预计1篇随笔)

附,Demo : http://files.cnblogs.com/files/08shiyan/ParallelDemo.zip

参见更多:随笔导读:同步与异步

(未完待续...)

.NET 同步与异步 之 警惕闭包(十)的更多相关文章

  1. .NET 同步与异步 之 Mutex (十二)

    本随笔续接:.NET 同步与异步 之 线程安全的集合 (十一) 本随笔 及 接下来的两篇随笔,将介绍 .NET 同步与异步系列 的最后一个大块知识点:WaitHandle家族. 抽象基类:WaitHa ...

  2. .NET 同步与异步 之 线程安全的集合 (十一)

    本随笔续接:.NET 同步与异步 之 警惕闭包(十) 无论之前说的锁.原子操作 还是 警惕闭包,都是为安全保驾护航,本篇随笔继续安全方面的主题:线程安全的集合. 先看一下命名空间:System.Col ...

  3. .NET 同步与异步 之 EventWaitHandle(Event通知) (十三)

    本随笔续接:.NET 同步与异步 之 Mutex (十二) 在前一篇我们已经提到过Mutex和本篇的主角们直接或间接继承自 WaitHandle: Mutex类,这个我们在上一篇已经讲过. Event ...

  4. 「JavaScript」同步、异步、回调执行顺序之经典闭包setTimeout分析

    聊聊同步.异步和回调 同步,异步,回调,我们傻傻分不清楚, 有一天,你找到公司刚来的程序员小T,跟他说:“我们要加个需求,你放下手里的事情优先支持,我会一直等你做完再离开”.小T微笑着答应了,眼角却滑 ...

  5. js同步、异步、回调的执行顺序以及闭包的理解

    首先,记住同步第一.异步第二.回调最末的口诀 公式表达:同步=>异步=>回调 看一道经典的面试题: for (var i = 0; i < 5; i++) { setTimeout( ...

  6. 同步、异步、回调执行顺序之经典闭包setTimeout分析

    聊聊同步.异步和回调 同步,异步,回调,我们傻傻分不清楚, 有一天,你找到公司刚来的程序员小T,跟他说:“我们要加个需求,你放下手里的事情优先支持,我会一直等你做完再离开”.小T微笑着答应了,眼角却滑 ...

  7. python学习笔记-(十四)I/O多路复用 阻塞、非阻塞、同步、异步

    1. 概念说明 1.1 用户空间与内核空间 现在操作系统都是采用虚拟存储器,那么对32位操作系统而言,它的寻址空间(虚拟存储空间)为4G(2的32次方).操作系统的核心是内核,独立于普通的应用程序,可 ...

  8. .NET同步与异步之相关背景知识(六)

    在之前的五篇随笔中,已经介绍了.NET 类库中实现并行的常见方式及其基本用法,当然.这些基本用法远远不能覆盖所有,也只能作为一个引子出现在这里.以下是前五篇随笔的目录: .NET 同步与异步之封装成T ...

  9. .NET 同步与异步之锁(Lock、Monitor)(七)

    本随笔续接:.NET同步与异步之相关背景知识(六) 在上一篇随笔中已经提到.解决竞争条件的典型方式就是加锁 ,那本篇随笔就重点来说一说.NET提供的最常用的锁 lock关键字 和 Monitor. 一 ...

随机推荐

  1. 接口隔离原则(Interface Segregation Principle, ISP)

    使用多个专门的接口,而不使用单一的总接口 接口隔离有两种定义: Clients should not be forced to depend upon interfaces that they don ...

  2. [转] Anaconda使用总结

    机器上的不同用户完全可以安装.配置自己的Anaconda,不会互相影响. 对于Mac.Linux系统,Anaconda安装好后,实际上就是在主目录下多了个文件夹(~/anaconda)而已,Windo ...

  3. BZOJ1180 [CROATIAN2009]OTOCI LCT

    欢迎访问~原文出处——博客园-zhouzhendong 去博客园看该题解 题目传送门 - BZOJ1180 本题和BZOJ2843一样. BZOJ2843 极地旅行社 LCT 题意概括 有n座岛 每座 ...

  4. 【Java】 剑指offer(27) 二叉树的镜像

    本文参考自<剑指offer>一书,代码采用Java语言. 更多:<剑指Offer>Java实现合集   题目 请完成一个函数,输入一个二叉树,该函数输出它的镜像. 思路 画图可 ...

  5. Mongodb - 二进制安装

    0.概述 mongodb版本:4.0.2 linux版本:redhat 6.5 安装方式:二进制安装 1.关闭防火墙 /etc/init.d/iptables status/etc/init.d/ip ...

  6. i春秋 “百度杯”CTF比赛 十月场 web题 Backdoor

    0x00: 打开题目,题目中告诉我们这题是文件泄露. 0x01: 通过扫描目录,发现可以扫到的有3个文件 index.php flag.php robots.txt 但是浏览flag.php它告诉我们 ...

  7. 开始使用Reflection

    用于 reflection 的类,如 Method,可以在 java.lang.relfect 包中找到.使用这些类的时候必须要遵循三个步骤:第一步是获得你想操作的类的 java.lang.Class ...

  8. ARM总线架构

    S3C2440集成了丰富了外设控制器(LCD控制器.USB Device控制器.USB Host控制器.NAND FLASH控制器.I2C控制器.SPI控制器等).要控制这些外设就要设置相应控制器的寄 ...

  9. c#基础在winform操作数据库,实现增删改查

    1.数据库操作类代码: using System; using System.Collections.Generic; using System.Linq; using System.Text; us ...

  10. Bzoj4480: [Jsoi2013]快乐的jyy 广义后缀自动机 倍增 哈希 manacher

    国际惯例的题面:有人说这是回文自动机的板子题,然而我是不会这种东西的.于是,我选择用更一般性的方法去解决这个题,就是那一堆东西了.首先,我们把两个串同时插入一个广义SAM里,拓扑排序维护每个节点的pa ...