文章吧-经典好文章在线阅读:CLR via C#读后感精选

当前的位置:文章吧 > 原创文章 > 原创精选 >

CLR via C#读后感精选

2020-11-12 00:38:22 来源:文章吧 阅读:载入中…

CLR via C#读后感精选

  《CLR via C#》是一本由(美)Jeffrey Richter著作,清华大学出版社出版的777图书,本书定价:99.00元,页数:2010-9,特精心从网络上整理的一些读者的读后感,希望对大家能有帮助。

  《CLR via C#》精选点评:

  ●也忘得差不多了 凡是微软的东西都忘得差不多了

  ●C# .net的经典中级书

  ●.net必读书目

  ●net圣经第3版

  ●这版本不错

  ●这是一本好书,所有想进阶的C#程序员必看的书.

  ●算是C#必读书目。搁了好几年,趁着做Unity3D再精进一下。

  ●除了好没什么好说的了

  ●经典,学习C#必读的!

  ●非常好,让我对更深层次有了一些理解。以后还需要读一遍。

  《CLR via C#》读后感(一):知其然,知其所以然

  本书的作者对C#的设计理念和设计细节了解透彻,他不仅教会你C#语言的各种细节,同时还把发生在背后的各种原理娓娓道来,细读本书之后,你学到的不仅仅是C#语言,同时还能够洞悉C#的本质,让你不仅仅是写出可以执行的代码,而是写出可以高效运行的代码。

  本书的译者不仅是一位技术大牛,同时也是一位对语言大牛,读本书的时候,你基本上感觉不到是在读一本翻译过来的著作,这种体验已经很久没有碰到过了。

  如果你只是想快速地使用C#写出代码,那么本书也许并不是特别适合你,但是如果你想提升在C#方面的造诣,那么本书必能让你如饥似渴。

  《CLR via C#》读后感(二):有空看看技术书~

  看点学业相关大体很有必要...虽然是多线程逼得不行了才下狠心看了看,结果到最后一章混合锁实在翻不下去,草草了事了。

  作者不愧是搞内核的大牛,讲起原理来头头是道,作为.net必读书目,恰如其分,不过如果不是专业程序员,大约看起来颇为鸡肋。

  要看这本书,大约还是需要点基础的,俺半年前刚开始看和现在看感觉都略有不同,当年都不知道有C++/CLI,实在是土得不行,脱离了C#的部分就颇有几分理解不能。

  等俺下回再翻翻吧。在俺看过的所谓技术书中算挺有收获的了,最起码比当年看起来一点都不入门的C#入门经典感觉好,至于每次改版页数都几乎线性增长,以至于塞不下都扔到光盘里的C#高级编程,还是当字典吧。

  推荐1,4章和GC部分。

  《CLR via C#》读后感(三):语言特性感想 和java对比

  读这本书重温java。

  1,event和method并列,解决了初学者容易写出紧耦合代码的问题,减轻了编写各种listener的痛苦。

  2,对const关键字的理解和java一致,不支持,它带来的麻烦超过了哪一点约束附加的好处。

  3,相对java增加了ref,struct等,property,operator overload,delegate,各种机制,增加了c#的学习曲线,感觉为了语法糖而增加了很多规则,值不值得呢?

  4,java8的lambeda表达式有必要吗?在已支持闭包在有eclipse ide各种自动生成代码的情况下值得吗?

  下面是我新写的另一篇和java对比评论。

  http://book.douban.com/review/7383663/

  《CLR via C#》读后感(四):每章重点和自己的思考 第三版

  最近工作比较闲,我把这本书看了两遍。

  第一遍是从17年3月份开始,断断续续直到上周读完,翻到最后一页的时候,心里如释重负,终于看完了,虽然这本书真的很厉害,但是看这么厚的书,真的很煎熬啊!然而,过了半天,我就忘记讲了什么,只记得委托是一个类(因为我会拿来跟同事装逼用:你知道委托其实说个类吗?不知道吧,哈哈哈!我满足的笑起来。),然而,我并没有梳理出脉络和框架,所以整本书就像各种细节组成的词典:看的时候发现原来这个成语是这个意思啊,原来还有这样的典故,可是过不了许久就只记得十之一二了。

  尤其是对我这种记忆力不好的人,从小背课文都没有完整的背下来过。因为记忆力不好,所以我学习东西必须要弄懂整个东西的脉络,从繁杂的细节中不断的梳理出主干,这样对记忆力的要求就低多了。慢慢的也就我就养成了这个习惯,总想梳理出脉络,整理出套路。

  回到CLR这本书,我觉得就是武功秘籍。发现有人评论说C++程序员也应该看一看,.Net虚拟机是如何解决那些C++也会遇到的问题的,这里蕴含了很多计算机理论的基础知识,我深以为然。所以,在看完第一遍的第二天,我就开始了第二遍的梳理,第二遍很快就看完了,边看边写下如下的笔记,夹杂着自己的一些理解和猜想。如果有人愿意瞥一眼,非常欢迎;如若发现错误,敬请不吝赐教!

  -------------第一部分 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常量);通过继承的方法可以定义公共字段的类型

  ublic 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类造成了功能困扰,如下:

  ublic static void Main(){

  Timer t = new Timer(MyTimerCallback, null, 0, 2000);

  Console.ReadKey();//此时t引用的Timer对象已经不可达,可以作为垃圾回收了;

  t.Dispose()//如果删掉此行代码,那么在Release版本中,TimerCallBack方法只会执行一次(因为在第一次调用时就把t的引用对象作为垃圾回收了);

  }

  rivate 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实例化对象时一定要先复制给临时变量,再用基元同步构造赋值给引用;

  ingleton 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>(无序);p760.

评价:

[匿名评论]登录注册

【读者发表的读后感】

查看CLR via C#读后感精选的全部评论>>

评论加载中……