目录
转自:CLR via C#--知乎,第三版第I部分 CLR基础
第1章 CLR的执行模型 3
1.1 将源代码编译成托管模块 3
1.2 将托管模块合并成程序集 6
1.3 加载公共语言运行时 8
1.4 执行程序集的代码 10
1.4.1 IL和验证 15
1.4.2 不安全的代码 16
1.5 本地代码生成器:NGen.exe 18
1.6 Framework类库 20
1.7 通用类型系统 22
1.8 公共语言规范 24
1.9 与非托管代码的互操作性 28
第2章 生成、打包、部署和管理应用程序及类型 29
2.1 .NET Framework部署目标 29
2.2 将类型生成到模块中 31响应文件 32
2.3 元数据概述 34
2.4 将模块合并成程序集 39
2.4.1 使用Visual Studio IDE将程序集添加到项目中 45
2.4.2 使用程序集链接器 46
.2.4.3 为程序集添加资源文件 48
2.5 程序集版本资源信息 49
2.6 语言文化 53
2.7 简单应用程序部署(私有部署的程序集) 54
2.8 简单管理控制(配置) 55
第3章 共享程序集和强命名程序集 59
3.1 两种程序集,两种部署 60
3.2 为程序集分配强名称 61
3.3 全局程序集缓存 65
3.4 在生成的程序集中引用一个强命名程序集 67
3.5 强命名程序集能防范篡改 69
3.6 延迟签名 70
3.7 私有部署强命名程序集 72
3.8 “运行时”如何解析类型引用 73
3.9 高级管理控制(配置) 76发布者策略控制 78
第II部分 设计类型
第4章 类型基础 83
4.1 所有类型都从System.Object派生 83
4.2 类型转换 85
4.3 命名空间和程序集 89
4.4 运行时的相互联系 92
第5章 基元类型、引用类型和值类型 101
5.1 编程语言的基元类型 101
5.2 引用类型和值类型 108
5.3 值类型的装箱和拆箱 113
5.3.1 使用接口更改已装箱值类型中的字段(以及为什么不应该这样做) 124
5.3.2 对象相等性和同一性 127
5.4 对象哈希码 129
5.5 dynamic基元类型 131
第6章 类型和成员基础 137
6.1 类型的各种成员 137
6.2 类型的可见性 140友元程序集 140
6.3 成员的可访问性 142
6.4 静态类 143
6.5 分部类、结构和接口 145
6.6 组件、多态和版本控制 146
6.6.1 CLR如何调用虚方法、属性和事件 148
6.6.2 合理使用类型的可见性和成员的可访问性 151
6.6.3 对类型进行版本控制时的虚方法的处理 154
第7章 常量和字段 159
7.1 常量 159
7.2 字段 160
第8章 方法 165
8.1 实例构造器和类(引用类型) 165
8.2 实例构造器和结构(值类型) 168
8.3 类型构造器 171
8.4 操作符重载方法 176
8.5 转换操作符方法 179
8.6 扩展方法 182
8.6.1 规则和原则 184
8.6.2 用扩展方法扩展各种类型 185
8.6.3 ExtensionAttribute类 187
8.7 分部方法 188
第9章 参数 191
9.1 可选参数和命名参数 191
9.1.1 规则和原则 192
9.1.2 DefaultParameterValueAttribute和OptionalAttribute 194
9.2 隐式类型的局部变量 194
9.3 以传引用的方式向方法传递参数 196
9.4 向方法传递可变数量的参数 201
9.5 参数和返回类型的指导原则 203
9.6 常量性 205
第10章 属性 207
10.1 无参属性 207
10.1.1 自动实现的属性 210
10.1.2 合理定义属性 211
10.1.3 对象和集合初始化器 214
10.1.4 匿名类型 215
10.1.5 System.Tuple类型 218
10.2 有参属性 220
10.3 调用属性访问器方法时的性能 225
10.4 属性访问器的可访问性 225
10.5 泛型属性访问器方法 225
第11章 事件 227
11.1 设计要公开事件的类型 228
11.1.1 第一步:定义类型来容纳所有需要发送给事件通知接收者的附加信息 229
11.1.2 第二步:定义事件成员 229
11.1.3 第三步:定义负责引发事件的方法来通知事件的登记对象 231
11.1.4 第四步:定义方法将输入转化为期望事件 233
11.2 编译器如何实现事件 233
11.3 设计侦听事件的类型 235
11.4 显式实现事件 237
第12章 泛型 241
12.1 Framework类库中的泛型 245
12.2 Wintellect的Power Collections库 246
12.3 泛型基础结构 247
12.3.1 开放类型和封闭类型 247
12.3.2 泛型类型和继承 249
12.3.3 泛型类型同一性 251
12.3.4 代码爆炸 252
12.4 泛型接口 252
12.5 泛型委托 253
12.6 委托和接口的逆变和协变泛型类型实参 254
12.7 泛型方法 256
12.8 泛型和其他成员 258
12.9 可验证性和约束 259
12.9.1 主要约束 261
12.9.2 次要约束 262
12.9.3 构造器约束 263
12.9.4 其他可验证性问题 264
第Ⅲ部分 基本类型
第13章 接口 267
13.1 类和接口继承 267
13.2 定义接口 268
13.3 继承接口 269
13.4 关于调用接口方法的更多探讨 271
13.5 隐式和显式接口方法实现(幕后发生的事情) 272
13.6 泛型接口 274
13.7 泛型和接口约束 276
13.8 实现多个具有相同方法名和签名的接口 277
13.9 用显式接口方法实现来增强编译时类型安全性 278
13.10 谨慎使用显式接口方法实现 280
13.11 设计:基类还是接口 282
第14章 字符、字符串和文本处理 287
14.1 字符 287
14.2 System.String类型 290
14.2.1 构造字符串 290
14.2.2 字符串是不可变的 292
14.2.3 比较字符串 293
14.2.4 字符串留用 298
14.2.5 字符串池 301
14.2.6 检查字符串中的字符和文本元素 301
14.2.7 其他字符串操作 303
14.3 高效率构造字符串 304
14.3.1 构造StringBuilder对象 304
14.3.2 StringBuilder的成员 305
14.4 获取对象的字符串表示:ToString 307
14.4.1 指定具体的格式和语言文化 308
14.4.2 将多个对象格式成一个字符串 311
14.4.3 提供定制格式化器 313
14.5 解析字符串来获取对象:Parse 315
14.6 编码:字符和字节的相互转换 317
14.6.1 字符和字节流的编码和解码 322
14.6.2 Base-64字符串编码和解码 323
14.7 安全字符串 324
第15章 枚举类型和位标志 327
15.1 枚举类型 327
15.2 位标志 332
15.3 向枚举类型添加方法 335
第16章 数组 337
16.1 初始化数组元素 339
16.2 数组转型 341
16.3 所有数组都隐式派生自System.Array 343
16.4 所有数组都隐式实现IEnumerable,Icollection和IList 344
16.5 数组的传递和返回 345
16.6 创建下限非零的数组 346
16.7 数组的访问性能 347
16.8 不安全的数组访问和固定大小的数组 351
第17章 委托 353
17.1 初识委托 353
17.2 用委托回调静态方法 355
17.3 用委托回调实例方法 357
17.4 委托揭秘 357
17.5 用委托回调许多方法(委托链) 361
17.5.1 C#对委托链的支持 365
17.5.2 取得对委托链调用的更多控制 365
17.6 委托定义太多(泛型委托) 368
17.7 C#为委托提供的简化语法 369
17.7.1 简化语法1:不需要构造委托对象 369
17.7.2 简化语法2:不需要定义回调方法 370
17.7.3 简化语法3:局部变量不需要手动包装到类中即可传给回调方法 373
17.8 委托和反射 375
第18章 定制attribute 379
18.1 使用定制attribute 379
18.2 定义自己的attribute类 382
18.3 attribute的构造器和字段/属性的数据类型 386
18.4 检测定制attribute 387
18.5 两个attribute实例的相互匹配 391
18.6 检测定制attribute时不创建从Attribute派生的对象 393
18.7 条件attribute类 396
第19章 可空值类型 399
19.1 C#对可空值类型的支持 401
19.2 C#的空接合操作符 403
19.3 CLR对可空值类型的特殊支持 404
19.3.1 可空值类型的装箱 404
19.3.2 可空值类型的拆箱 405
19.3.3 通过可空值类型调用GetType 405
19.3.4 通过可空值类型调用接口方法 405
第Ⅳ部分 核心机制
第20章 异常和状态管理 409
20.1 定义“异常” 409
20.2 异常处理机制 411
20.2.1 try块 412
20.2.2 catch块 412
20.2.3 finally块 414
20.3 System.Exception类 417
20.4 FCL定义的异常类 420
20.5 抛出异常 422
20.6 定义自己的异常类 423
20.7 用可靠性换取开发效率 425
20.8 指导原则和最佳实践 433
20.8.1 善用finally块 433
20.8.2 不要什么都捕捉 435
20.8.3 得体地从异常中恢复 436
20.8.4 发生不可恢复的异常时回滚部分完成的操作——维持状态 436
20.8.5 隐藏实现细节来维系契约 437
20.9 未处理的异常 440
20.10 对异常进行调试 444
20.11 异常处理的性能问题 446
20.12 约束执行区域(CER) 448
20.13 代码契约 451
第21章 自动内存管理(垃圾回收) 459
21.1 理解垃圾回收平台的基本工作原理 459
21.2 垃圾回收算法 463
21.3 垃圾回收与调试 466
21.4 使用终结操作来释放本地资源 469
21.4.1 使用CriticalFinalizerObject类型确保终结 470
21.4.2 SafeHandle类型及其派生类型 471
21.4.3 使用SafeHandle类型与非托管代码进行互操作 473
21.5 对托管资源使用终结操作 475
21.6 什么会导致Finalize方法被调用 477
21.7 终结操作揭秘 478
21.8 Dispose模式:强制对象清理资源 481
21.9 使用实现了Dispose模式的类型 485
21.10 C#的using语句 488
21.11 一个有趣的依赖性问题 490
21.12 手动监视和控制对象的生存期 491
21.13 对象复活 501
21.14 代 503
21.15 用于本地资源的其他垃圾回收功能 508
21.16 预测需求大量内存的操作能否成功 512
21.17 编程控制垃圾回收器 513
21.18 线程劫持 516
21.19 垃圾回收模式 517
21.20 大对象 520
21.21 监视垃圾回收 520
第22章 CLR寄宿和AppDomain 523
22.1 CLR寄宿 523
22.2 AppDomain 526
22.3 卸载AppDomain 538
22.4 监视AppDomain 540
22.5 AppDomain FirstChance异常通知 541
22.6 宿主如何使用AppDomain 541
22.6.1 可执行应用程序 542
22.6.2 Microsoft Silverlight富Internet应用程序 542
22.6.3 Microsoft ASP.NET Web窗体和XML Web服务应用程序 542
22.6.4 Microsoft SQL Server 543
22.6.5 更多的用法只局限于你自己的想象力 543
22.7 高级宿主控制 544
22.7.1 使用托管代码管理CLR 544
22.7.2 编写健壮的宿主应用程序 544
22.7.3 宿主如何拿回它的线程 546
第23章 程序集加载和反射 549
23.1 程序集加载 549
23.2 使用反射构建动态可扩展应用程序 554
23.3 反射的性能 555
23.3.1 发现程序集中定义的类型 556
23.3.2 类型对象的准确含义 556
23.3.3 构建Exception派生类型的一个层次结构 558
23.3.4 构造类型的实例 560
23.4 设计支持加载项的应用程序 562
23.5 使用反射发现类型的成员 564
23.5.1 发现类型成员 565
23.5.2 BindingFlags:筛选返回的成员种类 569
23.5.3 发现类型的接口 570
23.5.4 调用类型的成员 571
23.5.5 一次绑定,多次调用 575
23.5.6 使用绑定句柄来减少进程的内存耗用 581
第24章 运行时序列化 585
24.1 序列化/反序列化快速入门 586
24.2 使类型可序列化 590
24.3 控制序列化和反序列化 592
24.4 格式化器如何序列化类型实例 595
24.5 控制序列化/反序列化的数据 597
24.6 流上下文 603
24.7 将类型序列化为不同的类型以及将对象反序列化为不同的对象 604
24.8 序列化代理 606
代理选择器链 609
24.9 反序列化对象时重写程序集和/或类型 610
第Ⅴ部分 线程处理
第25章 线程基础 615
25.1 Windows为什么要支持线程 615
25.2 线程开销 616
25.3 停止疯狂 620
25.4 CPU发展趋势 622
25.5 NUMA架构的机器 623
25.6 CLR线程和Windows线程 625
25.7 使用专用线程执行异步的计算限制操作 625
25.8 使用线程的理由 627
25.9 线程调度和优先级 629
25.10 前台线程和后台线程 634
25.11 继续学习 635
第26章 计算限制的异步操作 637
26.1 CLR线程池基础 638
26.2 执行简单的计算限制操作 639
26.3 执行上下文 640
26.4 协作式取消 642
26.5 任务 645
26.5.1 等待任务完成并获取它的结果 646
26.5.2 取消任务 648
26.5.3 一个任务完成时自动启动一个新任务 649
26.5.4 任务可以启动子任务 651
26.5.5 任务内部揭秘 652
26.5.6 任务工厂 653
26.5.7 任务调度器 655
26.6 Parallel的静态For,ForEach和Invoke方法 657
26.7 并行语言集成查询(PLINQ) 660
26.8 执行定时计算限制操作 663
26.9 线程池如何管理线程 665
26.9.1 设置线程池限制 665
26.9.2 如何管理工作者线程 666
26.10 缓存线和伪共享 667
第27章 I/O限制的异步操作 671
27.1 Windows如何执行I/O操作 671
27.2 CLR的异步编程模型(APM) 675
27.3 AsyncEnumerator类 679
27.4 APM和异常 682
27.5 应用程序及其线程处理模型 683
27.6 异步实现服务器 687
27.7 APM和计算限制的操作 687
27.8 APM的注意事项 689
27.8.1 在没有线程池的前提下使用APM 689
27.8.2 总是调用EndXxx方法,而且只调用一次 690
27.8.3 调用EndXxx方法时总是使用相同的对象 690
27.8.4 为BeginXxx和EndXxx方法使用ref,out和params实参 691
27.8.5 不能取消异步I/O限制操作 691
27.8.6 内存消耗 691
27.8.7 有的I/O操作必须同步完成 691
27.8.8 FileStream特有的问题 692
27.9 I/O请求优先级 693
27.10 将IAsyncResult APM转换为Task 695
27.11 基于事件的异步模式 696
27.11.1 将EAP转换为Task 698
27.11.2 APM和EAP的对比 699
27.12 编程模型的泥沼 700
第28章 基元线程同步构造 703
28.1 类库和线程安全 705
28.2 基元用户模式和内核模式构造 706
28.3 用户模式构造 707
28.3.1 易失构造 708
28.3.2 互锁构造 713
28.3.3 实现简单的Spin Lock 717
28.3.4 Interlocked Anything模式 720
28.4 内核模式构造 722
28.4.1 Event构造 725
28.4.2 Semaphore构造 727
28.4.3 Mutex构造 728
28.4.4 在一个内核构造可用时调用一个方法 730
第29章 混合线程同步构造 733
29.1 一个简单的混合锁 733
29.2 自旋、线程所有权和递归 735
29.3 混合构造的大杂烩 737
29.3.1 ManualResetEventSlim类和SemaphoreSlim类 737
29.3.2 Monitor类和同步块 738
29.3.3 ReaderWriterLockSlim类 743
29.3.4 OneManyLock类 745
29.3.5 CountdownEvent类 747
29.3.6 Barrier类 747
29.3.7 线程同步构造小结 748
29.4 著名的双检锁技术 750
29.5 条件变量模式 754
29.6 用集合防止占有锁太长的时间 756
29.7 并发集合类 760
笔记
转自:每章重点和自己的思考 第三版--火赤炼 --知乎
-------------第一部分 CLR基础------------------------
第一章 CLR基础
1. CLR(Common Language Runtime)是一种提供了内存管理,程序集加载,安全性,异常处理和线程同步的运行库,个人觉得就是进程级别的虚拟机。
2. FCL(Framework Class Library)是一组包含了数千个类型定义的DLL程序集的统称,它包含在.NET Framework中。
3. Microsoft创造的面向CLR的语言编译器: C++/CLI,C#, VB, F#, Iron Python, Iron Ruby 和 IL中间语言,编译器将所有代码都编译成托管模块。
4. 托管模块是一个PE32或者PE32+(Portable Executable)文件,其主要包括构成:PE32(+)头信息(windows要求的标准信息),CLR头信息,元数据(代码级别)和IL语言。其中元数据包含(数据类型和成员的)引用表和定义表,及可能含有清单表。
5. 程序集是将托管模块和资源文件合并为一个逻辑性概念,它是重用、安全性以及版本控制的最小单元,其实简单的看就是exe和dll文件。合并的工具可以为各种编辑器或者程序集链接器。
6. IL语言可以看做为一种面向对象的机器语言,它完全公开了CLR的所有功能。
7. JIT(just-in-time即时编译器)把IL转换成本地CPU指令,这个发生在方法的首次调用时期。
8. 使用NGen.exe工具可以将IL语言转换为本地代码,这样可以避免运行时编译,但是也丢失了JIT针对执行环境的高度优化。
9. IIS等CLR宿主进程决定单个操作系统进程运行多少个AppDomain,默认情况下,每个托管EXE文件运行在一个独立的AppDomain中,一个AppDomain占用一个独立的地址空间。
10. Microsoft C#编译器允许编译直接操作内存的代码,但是要打开/unsafe编译器开关。
11. 为了统一面向CLR的语言之间的沟通,通过CLR交互,定义了CTS和CLS规范。
12. 为了与托管代码的交互,微软提供了三种交互形式:托管代码调用DLL中的非托管函数、托管代码可以使用COM组件、非托管代码可使用托管类型。
第二章 生成、打包、部署和管理应用程序及类型
1. 本章针对自用程序集。.NET Framework部署将允许用户控制安装软件,避免DLL hell 和 注册表。
2. CSC.exe文件能够编译脚本,响应文件(.rsp后缀)能够设置编译指令。
3. ILDasm.exe 反编译工具能够查看PE文件。
4. 程序集中必定有一个托管模块包含清单文件,清单文件也是一组元数据表的集合(表中主要包含了组成程序集的那些文件的名称,以及程序集的信息),CLR总是首先加载这个清单元数据表。
第三章 共享程序集和强命名程序集
1. 本章重点在于如何创建可由多个应用程序访问的程序集。
2. 为了能够实现程序集的更新和版本控制,采用公钥/私钥对程序集进行了签名(能够唯一标定程序集的开发商和版本),这就是强命名程序集 。
3. .NET 会逐步要求全部的程序集都要强命名。
-------------第二部分 设计类型------------------------
第四章 类型基础
1. 每个实例对象都有 类型对象指针 和 同步索引块。
2. 类型转换检查
if(o is Emplyee) { Employee e = (Employee) o; }//CLR其实进行了两次转换检查
Employee e = o as Emplyee
if(e != null){//}只有一次类型安全检查
1. 采用外部别名的方式来解决命名空间冲突问题。
2. 类型也是一种对象,类型对象的类型对象指针指向Type类型对象。
第五章 基元类型、引用类型和值类型
1. 基元类型大多数都是值类型(Int32等,但是String, Object等是引用类型);
2. 基元类型是指编译器直接支持的数据类型,基元类型在编译器和FCL类型中有完全映射,如int <--->System.Int32 , float <---> System.Single,并有IL指令支持;
3. 基元类型的溢出检查:checked操作符和指令都是对IL溢出检查指令的包装, CLR基元类型才有溢出检查。
4. decimal是C#封装出来的类,但不是CLR的基元类型,;
5. Object中Equals,GetHashCode,ToString,Finalize 为虚方法,GetType,MemberwiseClone 为非虚方法;
6. 值类型可以实现接口,值类型是隐式密封的,值类型继承自System.ValueType,枚举Enum继承自ValueType。
7. System.ValueType提供了与System.Object一样的方法,但是重写了Equals和GetHashCode方法,Finalize只有在垃圾回收的时候才会被调用。
8. LayoutKind.Sequential等类型排列特性能够保证字段在字段在内存中的排序。
9. C#认为值类型经常用于非托管代码操作,所以为值类型进行序列化特性,而对引用类型采用优化特性。
10. 装箱是指根据线程栈上的值类型生成托管堆中具有相同值的引用类型并返回其引用的过程;而拆箱是指获取引用类型中的原始值类型的指针,此指针还是指向托管堆。
11. 调用值类型的父类方法会造成装箱,如Object的方法包括实方法(GetType, MemberwiseClone)和ValueType中未被重写的虚方法(Equals, GetHashCode, ToString);
12. 接口类型变量必须指向的是堆上的引用,所以使用值类型的接口方法,就会自动装箱;
13. 如果需要装箱,请尽量显示装箱赋值,以避免多次隐式装箱;
14. c#中为了更改托管堆中已装箱值类型的字段,只能通过将装箱引用对象强制转换为接口类型才行,见 p144;
15. 由于更改值类型字段数值会带来很严重的拆装箱后果,所以建议将值类型字段设为不可变的readonly,事实上FCL核心值类型都是不可变的;
16. 对象的相等性和同一性,Objcect.ReferenceEquals比较的同一性,Object.Equals比较的同一性(应该比较相等性),ValueType.Equals比较的相等性(采用反射遍历实例字段);
17. Objcect和ValueType的实例方法GetHashCode效率都不高,重写了GetHashCode方法就不能提供唯一ID了,用RuntimeHelpers.GetHashCode静态方法可以获取对象的唯一ID;
18. dynamic能够在运行时绑定类型(也就是实现了动态语言的功能),能够运行时调用正确的方法;在使用dynamic时,如果类型实现了DynamicMetaObjectProvider接口,那么就会调用GetMetaObject方法,实现类型绑定,如果没有实现此接口就采用反射来执行操作;
第六章 类型和成员基础
1. 友元程序集,通过特性实现两个程序集之间Internal类型的可见;
2. 静态类,用static标定的不可实例化的类,C#编译器会自动将类标记为abstract和sealed;
3. IL指令中,call以非虚方式调用方法;callvirt会核查调用对象非空,而且以多态方式调用方法;
4. 虚方法速度比非虚方法慢,主要是由于callvirt检查,以及虚方法不能内联虚方法,所以尽量为简单的方法提供重载,而不是覆写。
5. C#编译器支持的partical 可以用于类,结构和接口。
第七章 常量和字段
1. Const常量会被编译器直接嵌入IL代码中,所以仅更新定义常量的程序集,并不会更改常量的实际值。
2. readonly 字段只能在构造函数中修改,static readonly 只能在静态构造函数中修改。
第八章 方法
1. 只有在没有定义任何构造函数的非Static类中,C#编译器才会隐式生成无参构造函数;
2. 采用MemberwiseClone方法和运行时反序列化工具生成类的实例时不会调用构造函数;
3. 类的实例字段在声明时赋值的简化语法在编译过程中会把赋值过程放到所有构造函数的在开始部分执行赋值,所以可能会导致代码膨胀;
4. C#编译器不会为值类型生成无参构造函数,甚至不允许显示定义无参构造函数,
5. 值类型允许定义有参构造函数,且任何构造函数都要初始化所有字段;
6. 值类型不允许对实例字段在声明时赋值(因为他不会生成默认构造函数),但是允许static字段声明时赋值;
7. 静态构造函数,又叫类型构造器,类型构造器不允许带参数,且不允许出现访问修饰符(强制为私有private),因为静态构造函数只能由CLR负责调用;
8. CLR编译方法时,如果其引用的类定义有类型构造器,且从未被调用,则会在此方法中生成代码调用类型构造器;
9. 类的静态字段在声明时赋值会导致C#在类型构造器中最开始调用赋值语句;值类型也支持静态字段的声明时赋值(不同于实例字段);
10. C#中在类型构造器中创建单例对象是最佳的单例模式实现方法;
11. 类型构造器的调用时间可以较大的影响代码性能,显示类型构造器比较耗费性能,而隐式生成的类型构造器可以较好的平衡性能;p195
12. CLR要求操作符重载必须是public和static方法,且C#编译器规定必须有参数与定义的类型相同;
13. CLR要求类型转换操作符必须是public和static方法,且C#编译器规定必须有参数或者返回值与定义的类型相同;用as is操作符时,不调用类型转换操作符;
14. C#扩展方法要求定义在*静态类中,;
15. 扩展方法不会检查调用方法的表达式的值是否为null,静态方法是通过ExtensionAttribute特性来标定扩展方法的;
16. 分部方法允许只声明不实现,这样编译器就会忽略编译,这也就导致分部方法不允许有返回值(void)或者out修饰参数符(因为方法可能不实现);
17. 分部方法默认为private,所以不允许添加作用域关键字;猜测原因:如果允许外部调用分部方法,将大大降低编译效率;
第九章 参数
1. 允许通过命名法传参;
2. ref,out可以作为方法重载的标签,但是ref 和out会视为同一个方法;
3. ref,out不支持类型隐式转换,为了保证类型安全;
4. 为了尽量扩大方法的复用性:声明方法的参数类型时,尽量指定为最弱的类型(接口弱于基类);相反,方法返回类型尽量声明为强类型;
第十章 属性
1. 对象初始化简化语法若调用无参构造器则可以省略小括号Employee e = new Employee{Name="He", Age = 45};
2. 集合初始化简化语法则是调用集合属性的Add方法;
3. 匿名类型一般之定义在方法内部,也不能泄露到方法外部;
4. System.Tuple类是一种泛型匿名类;
第十一章 事件
1. 事件的调用要考虑线程安全,事件的线程安全调用方法 EventHandler<T> temp = Interlocked.CompareExchange(ref NewEvent, null, null); if(temp != null)temp(this,e);P231
2. Event就是对private delegate加上线程安全的add,remove封装;
3. 显示实现事件:System.ComponentModel.EventHandlerList采用链表封装了一个委托池;也可以用哈希表,加上线程安全显示实现如p271
4. 由于委托语法糖不需要构造委托对象,事件也可以只用声明,而不用new实例化;
第十二章 泛型
1. 一种-多类型链表(每个节点的数据类型都可以不一样且保证类型安全的泛型链表)-采用继承的实现方式p250;
2. 泛型会引起代码爆炸,好在CLR内置了一些优化方式;
3. 泛型类型参数的三种形式:不变量,逆变量(in 标志),协变量(out标志);在定义泛型委托和泛型接口时,C#要求显示标记in,out类型参数,才能支持类型参数的隐式转换(逆变和协变);泛型类只支持不变量类型参数;p258
4. C#编译器支持泛型方法的类型推断,推断时根据的变量的类型(而不是变量的引用类型);
5. 泛型约束没有提供 枚举Enum等密封类型约束,好在可以在静态构造器中通过 typeof(T).IsEnum来判定;
6. 泛型方法只能根据类型参数的数量进行重载,而不能根据泛型约束;
7. C#中,属性、索引器、事件、操作符方法、构造器和终结器(finalizer)本身不能有类型参数(但是这些方法内部可以使用类型参数变量),因为实现他们的代价太大,而作用太小;
8. 泛型约束主要可以分为 变量类型约束(泛型类型必须是约束类及其派生类,或者实现了某个接口),变量关系约束(多个泛型类型变量之间必须有继承关系)和构造函数约束(只有一种约束where T:new()限定了类型必须有一个无参构造器);Nullable<T>类型不满足值类型struct约束;
9. 普通泛型类型的变量 == null比较不会报错,默认情况下,(值类型的变量==null) 永远为 false;
10. 泛型类型不能使用操作符(+、-、*、/),因为没有实现了操作符方法类型的约束;
第十三章 接口
1. 接口的实现方法默认为密封的,除非将实现方法显示标记为virtual;
2. 接口方法显示实现时,C#编译器要求隐式实现接口方法需要标记为public, 而显式实现接口方法默认为private(不标记),这样才能限制只有接口类型变量(实例变量不行)能调用显示实现方法;
3. 显示实现接口方法,在派生类中没办法调用(实验发现,base不能转换为接口,貌似base只能用来调用基类的公开方法和构造函数);
-------------第三部分 基本类型------------------------
第十四章 字符、字符串和文本处理
1. 三种方式实现Char与数值互转(按优越性排序):强制类型转换、System.Convert()、用Char实现了的IConvertible接口;
2. 字符串采用Ordinal模式的意思是逐字符比较(长度肯定相同,每个字符都相同,所以可以优化比较),字符串比较时,尽量采用忽略文化模式比较StringComparison.Ordinal或者StringComparison.OrdinalIgnoreCase,因为考虑语言文化比较最耗时(不同字符,不同长度的字符串也可能相同);
3. 微软对执行全大写字符串的比较进行了 优化,所以比较字符串大小尽量采用忽略文化,和忽略大小写(自动采用全大写比较)的模式;
4. 变化大小写,尽量采用ToUpperInvariant和ToLowerInvariant(对文化不敏感),而不是ToUpper或者ToLower(对文化敏感);
5. 字符串显式留用System.Intern(); Unity - CLR2.0默认留用;字符串留用的含义就是,相同的String引用都指向堆上同一个实例对象(以节约内存,这是通过哈希字典实现的,但是留用功能本身又比较耗性能和时间);
6. 字符串池是编译器对字符串文本的复用(将代码中相同的字符串文本合并元数据中的同一个字符串),而字符串留用是指同一个String对象;
7. ToString()和Parse要注意当前线程相关的CultureInfo,没有仔细看,用到的时候细看;
8. SecureString采用非托管内存缓冲区来保证加密数据的安全;
第十五章 枚举类型和位标志
1. 枚举不能定义任何方法,但是可以通过扩展方法来模拟添加方法;
2. Enum类型类似于一个结构体(一个公共实例字段<默认为int类型>,枚举项都是本类型的Const常量);通过继承的方法可以定义公共字段的类型
public Enum Color:byte{//等价于 public struct Color:System.Enum{
//隐含一个字段 public byte value__;
White, //等价于 Public const Color White = (Color)0;
}
第十六章 数组
1. 任何数组类型都是继承自System.Array类;
2. 一维0基数组(数组第一位为0)也被称为SZ数组或者向量,非0基数组开销很大,交叉数组[][](就是0基数组)的性能优于多维数组[ , ];
3. 数组类型转换只能在数组元素类型之间有隐式转换的前提下才能转换,所以值类型数组不能参与类型转换;
4. 用Array.Copy()方法能够实现任意类型的转换(类型安全的前提下,包括拆装箱,向下类型转换,比如Int32[]<--->Object[], Int32[]--->Double[]);
5. System.ConstrainedCopy()方法是保守的复制一个数组到另一个数组(元素的类型相同,或者从基类向派生类转换),System.Buffer.BlockCopy方法是按位兼容的数据数组的复制(如 Byte[] <--->Char[]),但是没有Copy方法的转型功能;
6. 所有数组隐式实现三种IEnumerable, ICollection和IList非泛型接口,所有0基一维数组还默认实现了他们的泛型接口;
7. 约定:当方法的返回值类型为数组时,保证返回数组类型(如果为空就返回空数组,而不是null);同样对数组类型的字段也最好有这个约定;
8. 非0基数组可以用Array.CreatInstance()方法创建;
9. 二维数组被视作非0基数组,安全访问(检查越界问题)二维数组最慢;交错数组安全较快,但是创建过程耗时,而且产生大量的类;非安全方式访问二维数组最快,但是限制使用;
10. 采用stackalloc语句可以在线程栈上分配数组(只能是一维0基、纯值类型元素构成的数组),这种数组性能最快p402;
11. 采用结构中内联数组的方式也能达到在线程栈上分配内存的目的,这种方式常用与非托管代码互操作p403;
第十七章 委托
1. CLR和C#都允许委托方法的协变性和逆变性;
2. 编译器将委托声明实现为一个委托类,委托类继承自MulticastDelegate类--继承自-->Delegate类--继承自-->Object;
3. 委托类的构造函数为两参构造函数(Object, IntPtr :分别为方法对象(如果是静态方法则为null)和方法指针)分别保存在MulticastDelegate 对应的字段中;
4. Delegate的静态方法Target和MethodInfo可以解析委托的上述两个字段;
5. MulticastDelegate 还有一个_invocationList字段用来保存委托链;
6. Delegate的静态方法Combine和Remove用来实现对委托链的操作;
7. 委托类自定义的Invoke方法能够遍历调用委托链的所有方法,但是方法不够健壮(只返回最后一个方法的返回值,一个方法出现问题,后面的都会堵塞),MulticastCastDelegate的实例方法GetInvocationList方法能够显示调用链中的每个委托;
8. 匿名函数允许操作当前方法的局部变量,但是它总是获得最新的变量值;
9. Delegate的静态方法簇CreateDelegate允许根据反射得到的方法信息(运行时才能确定的方法)来创建委托,而DynamicInvoke允许调用委托对象的回调方法传递一组运行时确定的参数;
10. 委托可以使用Ref, Out,Param方法,只是不能用FCL定义的Action等泛型委托;
第十八章 定制attribute
1. 定制attribute是类的一个实例,其类型从System.Attribute派生;
2. 利用Type.IsDefined()可以检查类与特性的关联;System.Attribute类的IsDefined(),GetCustomAttributes, GetCustomAttribute三个方法能够检测类和类型成员是否与某个Attribute关联;(确定了Attribute类型之后,还要再确定Attribute的字段值,才能最终确定特性的设置,然后据此逻辑执行分支实现特性的效果)
3. 为了确定Attribute实例的字段,Attribute类重写了Equals方法(采用反射来比较字段值),还提供了一个虚方法Match;
4. System.Reflection.CustomAttributeData类定义了GetCustomAttributes方法能够保证在检查定制特性时不执行特性类的构造方法或者访问器方法(执行这些方法会带来安全隐患,因为没有对当前AppDomain来说是未知的);
5. 条件Attribute,能够避免特性代码膨胀
Conditional("TEST") 对应代码中定义 #define TEST
第十九章 可空值类型
1. 在数据库中数值可以为空,而映射到FCL中没办法设置为空;另外Java的Data为引用类型, 而C#对应的DataTime为值类型,两者交互的时候也会出现类似问题;为了解决这个问题,就设计了可空值类型;
2. public struct Nullable<T>:T//可空值类型为值类型;
3. Nullable<Int32> x = null; 等同于 Int32? = null;
4. CLR和C#将可空值类型尽量表现为基元类型,支持相应值类型的各种操作:转换,转型,操作符,拆装箱等;
5. 空结合操作符?? String s = SomeMethod1()?? SomeMethod2()?? "Untitled";
6. CLR对可空值类型的装箱:装箱时检测(Int32? )a == null?{直接赋值null : 取出a的值,再装箱};拆箱亦然;
-------------第四部分 核心机制------------------------
第二十一章 自动内存管理GC
1. 值类型、集合类型、String、Attribute、Delegate和Exception类不用执行特殊的清理操作,他们自动回收垃圾;
2. GC判定非垃圾的第一步是查找根(线程栈、静态字段和CPU寄存器的引用变量)的对象,第二步再查上述对象的实例字段的引用对象;
3. 在方法中,一旦对象使用完毕(即后面的代码不再使用某对象),此对象就会作为垃圾(即使方法没有结束) ;但是这种情况下对调试器来说很不方便,所以微软VS编辑器做了修改,保证在调试Debug版本,这些局部变量都会存活知道方法出栈,但是Release版本依旧会回收;【注意】此设置对Timer类造成了功能困扰,如下:
Public static void Main(){
Timer t = new Timer(MyTimerCallback, null, 0, 2000);
Console.ReadKey();//此时t引用的Timer对象已经不可达,可以作为垃圾回收了;
t.Dispose()//如果删掉此行代码,那么在Release版本中,TimerCallBack方法只会执行一次(因为在第一次调用时就把t的引用对象作为垃圾回收了);
}
private static void TimerCallback(Object o){//调用垃圾回收
Console.WriteLine("Do Sth Here");
GC.Collect();
1. Finalize方法在对象对CLR确认是垃圾时自动调用,不同对象的Finalize的调用顺序不能得到保证;实现Finalize方法时需要注意:a.即使对象创建失败,CLR也可能调用Finalize()方法而造成错误,解决方案见 p475;b.由于不能保证Finalize()方法执行顺序,所以在Finalize()内部不能调用其他定义了Finalize方法的引用对象,因为其调用的对象可能已经提前回收了, Finalize方法中调用静态方法也要注意静态方法中的对象可能已经终结(Question?没弄明白);c.Finalize()方法可能因为内存不足JIT编译失败或者自身原因导致不执行;
2. 使用Finalize方法的几种场景:向主程序发布GC通知;回收本地资源(一定要手动关闭本地资源的句柄,否则会一直留在内存中;本地资源包括 文件、网络连接、套接字、互斥体等);
3. 为了保证本地资源被回收,针对Finalize方法的缺点,定义CriticalFinalizerObject类保证Finalize方法一定,且最后执行;在上述类的基础上,还定义了SafeHandle类进一步封装本地资源的句柄指针,并提供了引用计数器功能保证多线程不冲突;CriticalHandle类不提供引用计数器,但性能更好;
4. Finalize对GC周期的影响,定义了Finalize方法的对象在实例化对象时 会在终结列表添加一个对象的引用;GC时,扫描完所有根的引用后(终结列表的引用不算根),把终结列表中的垃圾对象引用转移到FReachable列表,此时对象及其字段引用对象都又复活;待所有对象扫描完毕,回收普通的对象内存;执行FReachable列表的Finalize方法,并移除引用,变成普通对象;下次GC时按照普通对象回收;
5. using语句等价于try{}finally{ IDisposable.Dispose();}的功能;
6. GCHandle类用来监视和控制对象生存期,有的能够影响对象周期,有的能够固定对象地址;另外fixed语句比用GCHandle类来生成一个句柄要高效;(固定句柄多用来固定对象地址,方便与非托管代码交互);
7.
GC.SuppressFinalize能将对象从终结列表中移除(以不再调用Finalize方法);GC.ReRegisterForFinalize()方法用于将对象放入终结列表(在下次GC时能够调用Finalize()方法);
8. GC分为三代0,1,2;0代的对象最新;垃圾回收器约定越新的对象活的周期越短;GCNotification实现了垃圾回收时通知p508;GC.Collection(n)可以指定回收第0到n代的垃圾,GC.WaitForPendingFinalizers()用于在调用Finalize方法时挂起所有线程;
9. 当引用的本地资源很大时,在需要GC清理垃圾时,需要主动提示GC实际内存消耗GC.AddMemoryPressure();以及限制本地资源数量HandleCollector类;
10. GC.MemoryFailPoint类能够在内存大量消耗的算法前检查内存是否充裕;
11. 在垃圾回收时为了保证托管代码的执行安全,通过 线程劫持(修改线程栈让线程挂起)或者保证线程指针执行到安全点(JIT编译指令表中标记的偏移位置),从而安全的移动对象在内存的位置;
12. 大对象总认为在第二代,大对象内存地址不会移动;
-------------第五部分 线程处理------------------------
第二十五章 线程基础
1. Windows为每个进程提供了至少一个专用线程,线程相当于逻辑CPU。p616;
2. 线程开销包括 内存耗用和时间开销。主要包含上下文thread context的线程内核对象、本地存储的线程环境块、用户模式栈、内核模式栈、DLL线程连接和分离通知(可以编码关闭),以及上下文切换(也就是CPU切换运行线程,这十分耗费性能), 要尽量避免上下文切换。p617
3. GC期间,CLR会挂起所有线程,然后检查每个线程的根。总结:线程创建、管理、销毁和上下文切换,以及垃圾回收的新能开销都和线程数量正相关,所以要尽量减少线程数量p619。
4. 最佳情况是一个CPU内核都有且只有一个线程,然而OS需要保证稳定性和响应能力,所以每个进程都会创建很多备用线程;
5. NUMA架构的计算机 能够缓解内存带宽对多核CPU性能的影响,然而CLR目前还不支持对NUMA架构的控制(非托管代码可以控制)。p624目前Win64只支持64核,Win32只支持32核。
6. 目前CLR线程直接对应一个Windows线程,但是将来可能将逻辑线程和物理线程分离,所以编程时尽量采用FCL库中的类型,以保证未来CLR变化时的兼容性。p625
7. 尽量采用线程池,而不是手动创建线程(new Thread()实例),除非满足如下任一条件(创建非普通优先级线程,创建前台线程,创建的线程会长时间运行,可能需要主动终结Abort线程)p626, 主线程调用新线程.Join()方法能够阻塞主线程直到被调用的线程销毁了;
8. 线程有0~32个优先级,当存在更高优先级线程准备好运行时,系统会立即挂起当前线程(即使后者的时间片没用完),这就是抢占式OS,它不能保证线程的执行时间。p632.
9. 为了逻辑清晰,将优先级分为进程优先级和线程优先级,而事实上,进程优先级是系统根据启动它的进程来分配的,而应用程序可以更改线程的相对优先级(Thread.Priority, p633)。
10. Thread.IsBackground属性将线程分为前台和后台,尽量使用后台线程:在进程中所有的前台线程都终结时,CLR会强制终于所有后台线程(线程池默认分配后台线程)。
第二十六章 计算限制的异步操作(就是不考虑线程同步的并行计算)
1. 创建和销毁线程是昂贵的操作,CLR采用启发式线程池类来管理线程,p638;线程池的线程分为工作者worker线程和I/O线程,一般使用 “异步编程模型APM”来发出I/O请求p639。
2. CLR默认线程池中,使用新线程的时会将上下文信息从调用线程复制到新线程,这会浪费性能,可以采用Threading.ExecutionContent类控制上下文的执行p640 。
3. 【协作式取消】.NET支持采用Threading.CancellationTokenSource类来取消新建的线程,(在主线程中调用CancellationTokenSource.Cancel方法,能够改变新线程中的CancellationToken.IsCancellationRequested属性)案例见p642;还可以注册取消CancellationTokenSource的回调方法(和执行线程);开可以建立关联CancellationTokenSource,实现联动取消。
4. 【工作项】异步工作项线程在线程池中调用 ThreadPool.QueueUserWorkItem(WaitCallback callback, Object state=null),p640;支持协作式取消。
5. 【任务Task】 为了解决QueueUserWorkItem方法发起的线程操作没办法知道操作在何时完成,以及没有返回值等缺陷,Microsoft引入了任务Task的概念(Threading.Tasks的Task类及Task<TResult>泛型类)。Task支持协作式取消。
6. Task的Wait(), WaitAll(), WaitAny(), Result等方法都会引出任务线程发出的异常(如果有的话,以集合异常AggregateException的形式封装),如果不调用的话,异常会一直留到GC的终结期Finalize()才抛出,这时候抛出的异常可以通过TaskScheduler.UnobservedTaskException()事件登记处理方法,如果没有登记的话,程序就会在这时中断。
7. Task支持CancellationTokenSource取消,支持任务链条,支持父子关系任务,还可以用任务工厂批量创建任务p653,最后还支持通过TaskScheduler类确定执行任务执行在 线程池的工作项线程(默认)或者同步上下文任务调度器Synchronization context task scheduler的GUI线程p655。
8. Parallel的静态For, ForEach和Invoke等多线程方法都是对任务Task的封装; PLINQ并行语言集成查询功能也是Task的封装p660;
9. Threading.Timer类通过线程池实现计时器(在一个线程池线程上延迟一定时间dueTime后以固定时间间隔period调用委托),Timer支持在内部更改dueTime和间隔period p663 , 如果调用的方法发生时间冲突,则会开启更多的线程(自己实验出来的)。
10. System.Windows.Forms的Timer类提供的计时器与Threading的Timer不同点在于,前者只在一个新线程中计时,而调用方法这设置计时器的那个线程。
11. 【线程池如何管理线程】尽管线程池提供了限制线程数量最大值的方法,但是尽量不要限制线程数量(可能发生死锁);
12. 【线程池优先调度Task】CLR线程池为每个工作者线程都分配了一个后入先出的本地队列用来放工作者线程调度的Task对象,此外还分配了一个先入先出的全局列表用来放普通工作项(由ThreadPool.QueueUserWorkItem方法和Timer生成)和非工作者线程调度的Task对象,一个工作者线程默认先处理本地对流的Task对象,然后帮忙处理其它工作者线程本地队列上的Task对象,最后才帮忙处理全局列表的普通工作项p667。
13. 【CPU缓存栈导致伪共享】CPU的缓冲区会缓存相邻的字节,可能导致不同内核数据之间需要通信,这反而会降低多线程的运行速度p668.
第二十七章 I/O限制的异步操作
1. 在Web应用中,Windows通过可以采用同步或者异步的方式来进行IO操作,系统为每个同步IO操作存入IRP队列(IO Request Packet)并开始进入睡眠时间,直到被IO操作结束操作系统唤醒线程并返回结果,如果客户端的请求越来越多,就会创建大量的线程导致线程自身及上下文切换占用了大量资源;异步IO操作需要在开启操作时声明回调方法,然后系统将操作信息存入驱动程序的IRP队列中,并把处理IRP结果的回调方法放入CLR的线程池队列中,待IO结束后启动线程池线程调用回调方法。
2. 【APM】CLR设计的异步编程模型APM(Asynchronous Programming Model)就是上述基于线程池回调方法的总结, APM的实现就是FCL类型中大量 有成对Begin_、 End_方法的类(包括委托中的BeginInvoke方法,案例见p688); 采用APM命名管道服务器-客户端案例p677;
3. 为了解决APM模型需要用很多回调方法的缺点,作者利用迭代器功能对APM进行封装实现了采用同步编程的异步操作类AsyncEnumerator【Question如何实现的】,案例见p681;
4. APM中发生异常时,CLR会把异常封装在IAsyncResult结果类中,并调用回调方法,需要在回调方法中处理异常;
5. 【GUI线程执行异步IO的回调函数】在GUI应用程序(Windows窗体,WPF, Silverlight)中只有创建了窗体的线程才能刷新这个程序的数据,而控制台程序(还包括ASP.NET Web窗体和XML Web服务)允许任何线程运行;因此维保了保证APM的回调方法人就运行在GUI线程中,FCL定义了同步上下文SynchronizationContext基类,它能够通过Post方法(主动返回,不等待)和Send方法(等待返回 ,阻塞线程池线程)保证回调函数运行在GUI线程上; p685页对其践行了简单封装并给出了案例;
6. 任何服务器都可以用APM实现异步服务器,采用AsynEnumerator类会更加简化编程;
7. 【不用线程池】有时候不能用线程池或者发起APM的主线程可能需要了解异步线程是否已经计算完毕,可以通过Begin_方法的IAsyncResult类型的返回值来进行查询,总共有三种方法:a. 在主线程中调用Begin_方法对应的End_方法<End方法只能调用一次,在回调方法中就不要再调用一次End方法了>; b.调用IAsynResult.AsyncWaitHandle.WaitOne方法;<a、b 这两个方法都会阻塞主线程,直到异步线程操作完毕返回>;c.在主线程中轮询IAsyncResult.IsCompleted<可以在轮询中加入Thread.Sleep降低CPU损耗>;
8. 只有调用了End_方法才能回收CLR为APM分配的资源,而且只能调用一次End_方法;
9. 【取消APM线程操作】APM一般不支持取消操作,但要看IAsyncResult对象是否支持;如果有大量特别快的IO,那就用同步IO操作,因为调用APM会产生一个IAsyncResult对象产生垃圾;
10. FileStream可以在实力化的时候指定以同步或异步方式通信,指定同步就用Read方法,指定异步就用BeginRead方法,如果混淆了会导致效率低下p692;
11. 【IO线程优先级】目前Windows系统支持指定IO线程的优先级,但是目前FCL还没有支持它,只能通过调用非托管代码的方式来设置,案例p693;
12. 【通过任务实现APM】通过任务工厂类Tasks.TaskFactory中的FromAsync方法可以实现通过任务执行I/O限制的异步操作,案例p695;
13. 【基于事件的异步模式EAP】开发团队认为基于IAsyncResult接口的APM对窗体开发人员太难了,就把它封装成了基于事件的异步模式EAP,它多用在基于界面开发的模块中(支持拖界面开发)p696;此外,任务也专门写了一个类TaskCompletionSource类来支持EAP,p699;
第二十八章 基元线程同步构造
1. FCL线程安全模式:对所有的静态方法保证线程安全,对实例方法都非线程安全,但是如果实例方法是为了协调线程,也要保证这种实例方法也是线程安全(例如CancellationToken和CancellationTokenSource类的字段要用volatile标记);p705
2. 基元线程同步构造有两种模式,用户模式和内核模式;p706
3. 【***用户模式 】是CLR直接通过特殊的CPU指令来操作线程,构造速度快,缺点是线程等待时一直在CPU上运行【活锁,浪费内存和CPU】:
4. 基元用户模式的同步线程构造有两个【易失构造(volatile)】和【互锁构造(Interlock)】 ,他们都可以对简单的数据类型的变量执行原子性读写操作和操作计时(内存栅栏);
5. 【原子操作】有的CPU架构需要内存对齐(内存偏移数为字段长度整数倍)才支持原子操作,一般情况下CLR保证字段正确对齐,除非用FieldOffsetAttribute特性的Offset指定不对齐;
6. 【【内存栅栏】】(Volatile和Interlocked都支持,又叫做操作计时 )指的是 运行时按照代码顺序执行读写操作(有时候C#编译器,JIT编译器和CPU都会对代码进行优化,导致读写操作顺便变化,以及编译不执行代码等,在单线程中的优化没有问题,但是多线程中就会出现 运行时 bug,p709),它还会阻止字段进入CPU缓存(Cha26,共享字段在伪共享中会造成伪共享);
7. 【Volatile】可以分拆为VolatileRead、VolatileWrite和MemoryBarrier三个子功能;以out ref传引用方式传volatile的值将会失去易失构造特性(p713);个人理解是易失操作都是针对变量做的标记,如果传递引用就新建了一个变量;
8. 【静态类Interlocked】中每一个方法都保证原子操作和内存栅栏,对不同类型支持 加减乘除Exchange/CompareExchange等功能,通过Interlocked类可通过对Int32值类型的操作 用来在不阻塞线程的情况保证一个方法只被一个线程调用等功能;见案例p714;
9. 【自旋锁SpinLock】通过Interlocked可以构造一个自旋锁(用While方法不停巡视是否拿到许可),用来实现代码区块的同步,案例SimpleSpinLock见p717;自旋锁浪费CPU时间,它只用来保护执行非常快的区域,且最好不用在单CPU机器,自旋锁线程的优先级要尽量低(禁止操作系统自动动态的提升线程优先级);支持lockTaken模式;
10. 【BlackMagic】为了减缓自旋锁的CPU占用,FCL提供了Threading.SpinWait结构体;这个结构体采用了Thread.Sleep(0), Thread.Sleep(1), Thread.Yield()和Thread.SpinWait()四个方法暂停线程(根据方法不同,确定是否切换上下文);
11. 【自定义Interlocked方法】Interlocked.CompareExchange()方法有Int32, Int64, Single, Double, Object和泛型引用类型多个重载版本,基于它们可以实现Multiple,Divide,Minimum,Maximum, And, Or, Xor等方法,见案例p720Maximum; 作者甚至写了一个泛型方法p721;
12. 【【内核模式】】是Windows操作系统内核中实现的函数,它能够让线程在等待时阻塞线程【死锁,只浪费内存,好于活锁】,缺点是锁构造慢(代码要在托管和本地内核模式之间切换);此外线程在用户模式和内核模式之间切换会招致巨大的性能损失。p722
13. 基元内核模式的线程同步构造有两个【事件】和【信号量】,其他的内核模式都是对它们的封装(包括互斥体);p722;
14. 【WaitHandle】内核模式的核心是Threading.WaitHandle抽象基类,在内核对象的构造过程的所有方法都保证内存栅栏,而WaitHandler提供了对内核对象线程安全的访问方法(Dispose, WaitOne, WaitAny, WaitAll, SignalAndWait等),操作系统会根据情况自动线程阻塞;
15. 【事件Event】构造就是继承WaitHandle且内核维护的Boolean变量的封装,如果事件为false,就阻塞线程,如果事件为true,解除阻塞;根据设置变量的形式,又衍生出自动重置事件(AutoResetEvent一次只能释放一个阻塞线程)和一个手动重置事件(ManualResetEvent 可以释放全部的阻塞线程);
16. 【信号量Semaphore】构造就是继承WaitHandle且内核维护的Int32变量,信号量为0时,就阻塞线程;信号量大于0时,解除阻塞;当解除一个阻塞线程内核就自动减1,而调用 Release()方法内核变量加1;通过设置信号量初始值可以设定释放阻塞线程的数量;p727
17. 【互斥体Mutex】类似于一个AutoResetEvent,因为它一次只释放一个阻塞线程,但是还有额外的功能就是线程所有权: 通过维护线程ID, 保证调用线程就是Mutex的那个线程,而且实现了递归锁(线程从一个带锁的方法进入另一个带锁的方法);案例用AutoResetEvent实现了一个递归锁,建议用这个递归锁(因为托管代码的实现可以减少与内核的切换,效率更高);p729;
18. 【内核对象构造的回调方法】通过ThreadPool.RegisteredWaitHandleDemo方法能注册一个在内核对象构造完成时候的回调方法,这样就可以避免Wait等方法的调用,可以节约内存;p731
第二十九章 混合线程同步构造
1. 混合线程同步构造Hybrid thread synchronization construct是综合了用户模式和内核模式的构造来构建的,它能够综合基元用户模式构造在没有竞争时的高效,和有竞争时基元内核模式下线程不自旋节约CPU的优点;提供了一个最简单的混合线程同步锁p733;
2. 通过给等待期间增加一小段自旋时间能够减少内核模式的切换,可能能够进一步提高性能,另外作者给锁增加了所有权,线程递归等功能,p735;
3. FCL提供了很多混合锁,他们的功能不一,有的推迟内核模式的构造到第一次竞争时,还能够支持协作式取消CancellationToken(ManualResetEventSlim和SemaphoreSlim类);p737
4. 【Monitor,同步块】静态类是最常用的混合线程同步构造,它的作用是维护内存中的一个同步块(sync block)队列(每个同步块包含一个内核对象、线程ID、递归计数 和一个等待线程计数);在构造对象时,同步块索引指向-1,调用Monitor.Enter(Object)方法时,CLR将对象的同步块索引指向一个新的同步块,并更新同步块的数据;当再次调用Monitor.Enter方法时更新递归计数或者等待线程计数;当调用Monitor.Exit方法时,会检查等待线程,重置计数或者设置对象的同步块索引为-1;p738
5. 现有的Monitor非常容易导致线程堵塞,而且难以调试,示例p740;为了避免这个问题,强烈建议专门设置一个私有对象的同步块索引作为同步锁,一般就用Object对象;示例p740
6. 【lock是Monitor,try ,finally的语法糖】C#语言提供了lock关键字类简化Monitor同步锁, 它用Finally来保证Monitor.Exit是一件非常不好的做法,因为这样会隐藏线程异常,让程序带病运行,p742; 如果线程在进入try块,而在调用Monitor.Enter方法钱退出,那么可以通过lockTaken变量(Boolean类型)来确定在Fanilly块中要不要调用Monitor.Exit方法;
7. 【读写锁】ReaderWriterLockSlim类是一个读写锁构造,读取线程可以同步执行,但会阻塞写入线程,写入线程会堵塞其它写入线程和读取线程;ReaderWriterLockSlim类可以支持递归(代价很高,需要一个互斥自旋锁),支持将reader级别提升为write级别(代价很高,不建议使用),此外已经废弃了性能很差的ReaderWriterLock构造。p743
8. 【自定义读写锁】作者基于Interlocked类操作位bit 实现了OneManyLock类的读写锁,性能高于FCL提供的读写锁,p745;
9. CountdownEvent类 类似于Semaphore;p747
10. Barrier类 能够让多个线程按照阶段运行,等待其他线程都完成了一个阶段之后,再一起进入下一个阶段;p748
11. 【多线程单例】在单例模式中,双检锁是指两次if判定是否为空;有两点指的关注,由于C#有内存栅栏,可以保证CPU缓存的s_Value变量一定是真实的(Java的锁没有内存栅栏);new实例化对象时一定要先复制给临时变量,再用基元同步构造赋值给引用;
Singleton temp = new Singleton();
Interlocked.Exchange(ref s_value, temp);
//s_value = new Singleton()//编译器可能先在内存中分配一块地址给s_value,然后再给内存调用构造器,这期间另外一个线程可能 就会使用这个不完整的内存对象,这个bug一般不可重复。
1. 【最好的单例】其实就是直接用默认类型构造函数,private static Singleton s_value = new Singleton();此外还有利用Interlocked.CompareExchange技术将实例化放到普通静态属性中的单例模式;p752
2. 泛型类Lazy<T>将线程安全单例的三种方式进行了封装(适合GUI程序而不考虑线程安全的模式,双检锁技术,Interlocked.CompareExchange技术);同样的还有Threading.LazyInitializer类;
3. 当希望一个线程在条件为true的时候执行代码,如果一直自旋判定条件非常耗费性能,可以通过给条件加内核基元锁实现,实现了一个线程安全且检查队列长度的队列Queue, p755;
4. 【集合改造读写锁】在服务器的读写锁中,当一个写锁锁定资源,如果新来的读取请求很多,它们只会新建线程并堵塞;当写入线程释放锁时,大量的读取线程会导致严重的上下文切换;为了解决这个问题,采用Berrier类、Task以及队列 来分批次控制读取线程的创建;作者由此发明了ReaderWriterGate和AsyncGate类;p759;
5. 【并发集合类】FCL自带四个线程安全集合类: ConcurrentQueue <T>(FIFO),ConcurrentStack<T>(LIFO), ConcurrentBag<T>(无序),ConcurrentDictionary<TK,TV>(无序);