CLR via C# (第三版)讀後總結

版權歸作者所有,任何形式轉載請聯繫作者。
作者:V_火赤煉(來自豆瓣)
來源:https://book.douban.com/review/8609352/


最近工作比較閒,我把這本書看了兩遍。
第一遍是從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常量);通過繼承的方法可以定義公共字段的類型 

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>(無序);p760.
© 本文版權歸作者  V_火赤煉  所有,任何形式轉載請聯繫作者。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章