本随笔续接:.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. hdu 4707 仓鼠 记录深度 (BFS)

    题意:linji的仓鼠丢了,他要找回仓鼠,他在房间0放了一块奶酪,按照抓鼠手册所说,这块奶酪可以吸引距离它D的仓鼠,但是仓鼠还是没有出现,现在给出一张关系图,表示各个房间的关系,相邻房间距离为1,而且 ...

  2. [转] 深入理解React 组件状态(State)

    React 的核心思想是组件化的思想,应用由组件搭建而成,而组件中最重要的概念是State(状态),State是一个组件的UI数据模型,是组件渲染时的数据依据. 一. 如何定义State 定义一个合适 ...

  3. 【Java】 剑指offer(5) 从尾到头打印链表

    本文参考自<剑指offer>一书,代码采用Java语言. 更多:<剑指Offer>Java实现合集 题目 输入一个链表的头结点,从尾到头反过来打印出每个结点的值.结点定义如下: ...

  4. 细说Vue作用域插槽,匹配应用场景。

    最近在官方文档中看到,vue新增了一种插槽机制,叫做作用域插槽.要求的版本是2.1.0+. 首先来说一下:顾名思义,所谓作用域插槽,主要就在作用域,需要注意的是(以下几点看不懂不要紧,配合下面的例子, ...

  5. Java实验-课程设计报告一:个人银行账户管理系统SavingAccountManageSystem-具体文档+源码

    课程设计报告一:个人银行账户管理系统 此文档及源码仅供参考 不得直接复制使用 author: [xxxxxxxxx xx xxxx] date: "2019-04-12" 作 者: ...

  6. Windows环境下 PHP调用R脚本

    写在前面的: 由于是windows平台实现的,只要保证脚本命令能在cmd控制台运行,则可以在php中利用system()实现. 注意事项: (1).保证system的路径中无汉字和空格 !!  (亲身 ...

  7. Leaflet_扩展Leaflet:类(2017-10-26)

    扩展教程:http://leafletjs.com/examples/extending/extending-1-classes.html 翻译 该教程介绍扩展Leaflet最常用的方式. 注意:本文 ...

  8. CocosCreator编辑器界面

    1,资源管理器[参考来源:官方文档] 资源管理器 里显示了项目资源文件夹(assets)中的所有资源.这里会以树状结构显示文件夹并自动同步在操作系统中对项目资源文件夹内容的修改.您可以将文件从项目外面 ...

  9. 【转载】IntelliJ IDEA 内存优化最佳实践

    本文转自 http://blog.oneapm.com/apm-tech/426.html [编者按]本文作者在和同事的一次讨论中发现,对 IntelliJ IDEA 内存采用不同的设置方案,会对 I ...

  10. C#线程篇---Task(任务)和线程池不得不说的秘密

    我们要知道的是,QueueUserWorkItem这个技术存在许多限制.其中最大的问题是没有一个内建的机制让你知道操作在什么时候完成,也没有一个机制在操作完成是获得一个返回值,这些问题使得我们都不敢启 ...