C#知識點記錄

C#知識點記錄

 

簽名生成

E:\Visual Studio 2013\Common7\IDE\ItemTemplates\CSharp\Code\2052\Class 文件生成簽名
//-------------------------------------------- 
// Copyright (C) 公司名
// filename :$safeitemrootname$ 
// created by 做者 
// at $time$ 
//--------------------------------------------html


CLR:公共語言運行時(Common Language Runtime)是一個可由多種編程語言(VB、F#等)使用的公共語言運行庫。算法

託管模塊:編譯源碼會生成託管模塊,他是標準的PE文件。包含:PE32頭、CLR頭、IL中間語言。數據庫

元數據:CLR除了生成IL外,還會生成元數據。元數據總與包含IL代碼的文件相關聯。編程

程序集:是一個或多個模塊/資源的邏輯性分組。是重用、安全性、版本控制的最小單元。程序集能夠是DLL或者可執行程序。windows

FCL:Framework類庫(Framework Class Library)一組DLL程序集的統稱。設計模式

CTS:通用類型系統,制定的一個正式規範描述類型定義和行爲的系統。數組

CLS:公共語言規範,詳細定義了一個最小功能集。只有支持這個功能集,生成的類型才能兼容其餘組件。緩存

OOP:面向對象編程(Object-Oriented Programming)軟件的一種設計模式,基於封裝、繼承、多態這三個概念進行開發。安全

CSP:組件軟件編程(Component Software Programming)OOP發展到極致的產物,講代碼造成組件模塊。服務器

JIT: 即時編譯器 

 

兩種程序集

CLR支持兩種程序集:弱命名程序集 和 強命名程序集。

他們結構徹底相同,均包含PE文件、PE32頭、CLR頭、元數據、清單表、IL。

區別在於強命名程序集使用發佈者的公鑰/私鑰進行了非對稱加密簽名。能夠部署到任何地方。

兩種部署

程序集能夠採用兩種方法進行部署:私有或全局。

私有部署指應用程序目錄或某個子目錄。弱命名程序集只能私有方式進行部署。

全局部署指部署到一些公認位置的程序集。CLR在查找程序集時,會檢查這些位置。

CLR要求每一個類型最終都從 System.Object 類型派生。因此每一個類型都會有基本方法。

類型安全:CLR最重要的特性之一,運行時CLR總能知道對象的類型是什麼。調用GetType便可知道對象的確切類型。

類型轉換

隱式轉換與顯式轉換。如派生類轉換爲基類,稱爲隱式轉換,不須要操做。

//隱式轉換
Object o = new Int32();
//顯式轉換
Int32 i = (Int32)o;

is 和 as 操做符轉換。

is 判斷是否能夠轉換。能夠返回 true。

as 是is的簡化寫法。會判斷是否能夠轉換,若是不能夠返回null,能夠則返回轉換後的結果。

基元類型:編譯器直接支持的數據類型稱爲基元類型,編譯器會自動映射到對應對象。例如string 會映射到 System.String

checked 和 unchecked 基元操做

對基元類型進行算術運算操做,能夠形成溢出。編譯器默認不會檢查溢出,checked操做符進行檢查。unchecked操做符不進行檢查。

checked
{
     Byte b = 100;
     b = (Byte)(b + 300);
}

uint i = unchecked((UInt16)(-1));

CLR支持兩種類型:引用類型和值類型。

引用類型:從託管堆分配,new操做符返回對象內存地址。每次初始化都會進行內存分配,因此效率會變慢。

值類型:通常在線程棧上,不包含指向指針。不受GC控制,緩解了託管堆的壓力。

任何稱爲類的類型,都是引用類型。值類型都稱爲結構或枚舉。

值類型比引用類型輕的緣由是,不用分配內存,不被GC,不會經過指針引用。

裝箱:值類型轉換成引用類型,稱爲裝箱。分別進行三步:1.分配內存。2.賦值字段到堆中。3.返回對象地址。

拆箱:引用類型轉換值類型,稱爲拆箱。分別進行兩步:1.獲取已裝箱的地址。2.將字段包含的值從堆複製到棧的實例中。

拆箱效率比裝箱低得多。

哈希碼:Object提供GetHashCode能夠得到任何對象的哈希碼。

dynamic基元類型:表示對象的操做將在運行的時候解析。能夠繞過類型安全監測。

static void Main(string[] args)
        {
            dynamic value;
            for (int demo = 0; demo < 2; demo++)
            {
                value = (demo == 0) ? (dynamic)5 : (dynamic)"A";
                M(value);
            }

            Console.ReadLine();
        }

        private static void M(int n) { Console.WriteLine("Int類型{0}", n); }
        private static void M(string s) { Console.WriteLine("String類型{0}",s); }

成員的可訪問性

private  只能由定義類型或嵌套類型的方法訪問。

protected  只能由定義類型、嵌套類型、派生類型中訪問。

internal  只能由定義程序集中的方法訪問

protected internal 任何嵌套、派生、程序集中的方法訪問。

public   能夠由任何程序集訪問。 

靜態類:static 定義不可實例化的類。不能應用於結構。靜態方法不會進行GC。

靜態類不能建立實例、不能實現任何接口、只能定義靜態成員、不能做爲字段,方法參數或局部變量使用。

靜態類從程序啓動時就會一直佔用內存。只有肯定不須要實例化、繼承等特性的時候,才使用靜態類。

分部類:partial能夠將代碼分散到一個或多個源代碼中。

分部類的優勢:源代碼控制方便,在同一個類中分解成不一樣的邏輯單元,代碼拆分。

組件版本控制關鍵字

abstract  抽象方法,指派生類型必須重寫並實現這個成員。抽象類與接口的區別就是:is-a 與 can-do 的區別。繼承 Is a、接口 Can do、組合 Has a

virtual  虛方法,指已定義實現,可由派生類型重寫。

override  實現抽象,表示正在重寫類型的成員。

sealed  密封類,不能被派生重寫。

new   表示該成員與基類中類似的成員無關係。public new void ToString()

常量:定義之後不能修改。public const int result = 3;

只讀:運行時能夠賦值一次。public readonly int result = 3;

字段修飾符

static   靜態字段

const  常量字段,只在聲明的時候寫入。

readonly  只讀字段,只在構造的時候寫入。

volatile  易變類型,保證原子性讀取

構造方法在元數據表中,始終叫作.ctor。構造器,不能被繼承。因此不能添加版本控制關鍵字。

抽象類默認構造函數訪問爲protected。其他均默認爲public。靜態類沒有構造函數。

this()可顯示調用另外一個此類中另外一個構造函數。public SomeType(string x):this(){}

base()可顯示調用基類的構造函數。public SomeType():base(){}

靜態類也能夠添加構造方法,他只會被第一次調用類成員以前被運行,而且不能帶任何參數。

static class SampleClass
    {
        static string name;
        static SampleClass()
        {
            name = "靜態構造函數";
        }

        public static void Method()
        {
            Console.WriteLine(name);
        }
    }

操做符重載

在類裏面能夠重載操做符,重載後可直接對類進行操做。

重載方法必須是public 和 static方法。添加operator標誌,來進行操做符選擇。

Complex co1 = new Complex(1, 2);
Complex co2 = new Complex(3, 4);
Console.WriteLine((co1 + co2).a);

public static Complex operator +(Complex c1, Complex c2)
        {
            c1.a += c2.b;
            return c1;
        }

轉換操做符重載

在類裏面能夠重載轉換操做符,重載後可顯示或隱式轉換類。

轉換操做符必須是public 和 static方法,而且必須使用operator標記。

隱式轉換:implicit

public static implicit operator Rational(Int32 num)
        {
            return new Rational(num);
        }

 Rational r1 = 5;

顯式轉換:explicit

public static explicit operator Int32(Rational r)
        {
            return r.ToInt32();
        }

Int32 r2 = (Int32)r1;

擴展方法

C#只支持擴展方法,不能擴展屬性、事件、操做符等等。

擴展方法第一個參數前面必須有this。必須在非泛型靜態類中聲明。靜態類必須爲頂級靜態類,單獨一個文件做用域。

若是命名空間不一樣,則須要using引用dll。

下面是StringBuilder的擴展,添加了一個show方法

public static class StringBuilderExtensions {
        public static void Show(this StringBuilder sb)
        {
            Console.WriteLine("這是擴展方法,{0}",sb.ToString());
        }
    }

StringBuilder sb = new StringBuilder("我是參數");
sb.Show();

泛型接口擴展

public static void ShowItems<T>(this IEnumerable<T> collection)
        {
            foreach (var item in collection)
            {
                Console.WriteLine(item);
            }
        }

"Chenxy".ShowItems();

委託擴展

public static void InvokeAndCatch<T>(this Action<object> d, Object o) where T : Exception
        {
            try
            { d(o); }
            catch (T)
            {  }
        }

//Action<Object> action = delegate (object o) { Console.WriteLine(o.GetType()); };
            Action<Object> action = o => Console.WriteLine(o.GetType());
            action.InvokeAndCatch<NullReferenceException>(null);

稍微說說這個委託擴展,這個的效果就是把空對象這個異常類給吞噬了。而後採用匿名或者lambda調用。

可選參數:參數後面使用 = 賦值。則調用的時候,如未填寫此參數,自動賦值默認參數。DateTime dt = default(DateTime)

default:給類型賦值默認值。

命名參數:調用方法時,可以使用名字進行選擇性賦值。例如:M(s: "B");  public void M(String s = "A")

隱式變量:var 要求編譯器根據表達式推斷具體數據類型。只能聲明在方法內部。

out:不期望調用者在調用方法以前初始化好對象。被調用的方法不能讀取參數的值,須要在返回前寫入這個值。

ref:調用者必須在調用方法前初始化參數的值,被調用的方法能夠讀取值以及寫入。

可變數量:params 只能用於方法參數的最後一個。能夠傳遞多個相同類型的參數。params Object[] objects

聲明方法參數類型,應指定最弱類型,寧願要接口也不要基類。

例如:處理一組數據,參數最好用接口 IEnumerable<T> 不要使用強類型List 或 ICollection

緣由:能夠傳任何繼承IEnumberable接口的類型,例如字符串。更加靈活,適合更普遍的場景。

儘可能作到參數基類,返回值派生類。例如

//
        public void Main<T>(IEnumerable<T> collection) { }
        //很差
        public void Main<T>(IList<T> collection) { }

        //
        public void Pro(Stream str) { }
        //很差
        public void Pro(FileStream str) { }

        //
        public FileStream Open() { }
        //很差
        public Stream Open() { }

面向對象設計原則之一數據封裝,意味着類型的字段永遠不該該公開,不然很容易破壞對象的狀態。

強烈建議將全部字段都設爲private。若是容許用戶獲取或設置,則公開一個方法。稱爲訪問器(accessor)

自動實現屬性:封裝字段的操做,並定義獲取和設置方法。public string name {get;set;}

對象初始化器:構造一個對象並初始化一些公共屬性(字段)。

Employee em = new Employee() { name = "", old = "" };
            //能夠省略前面的括號
            Employee em1 = new Employee { name = "", old = "" };
            //集合初始化器
            var table = new Dictionary<string, int>
            {
                { "",1 },
                { "",2 }
            };

匿名類型:能夠用很簡潔的語法來自動聲明不可變的含有一組屬性的類型。

//匿名類型
            var ol = new { Name = "Chenxy", Year = 1993 };
            //匿名數組
            var people = new[] {
                new { Name="Chenxy",Year=1993 },
                new { Name="Chenxy1",Year=1992 }
            };

匿名類型能夠配合LINQ作投影

IEnumerable<Employee> list = new List<Employee> { new Employee { name="chenxy",old="1991" }, new Employee { name = "chenxy", old = "1992" } };
            var query = from li in list select new { Year = li.old };
            foreach (var item in query)
            {
                Console.WriteLine(item.Year);
            }

索引器重載:對類型的[] 操做進行重載。須要使用this關鍵字來進行操做。

public Employee this[Int32 i]
        {
            get
            {
                return new Employee { name = "索引方法", old = i.ToString() };
            }
            set { }
        }

var query = new Employee() { };
Console.WriteLine(query[1].old);

事件類型

事件:定義事件成員的類型容許通知其餘對象發生了特定的事情。例如點擊後執行方法,就是經過事件實現。

事件模型以委託爲基礎,委託是調用回調方法的一個類型安全的方式。對象憑藉回調方法接收通知。

主要是使用委託機制,在以前配置好事件調用,而後收到新信息的時候,執行委託。約定消息定義後綴使用EventArgs

 //消息
    internal class NewEventArgs : EventArgs
    {
        public string str_name { get; set; }
        public NewEventArgs(string name)
        {
            str_name = name;
        }
    }

    //委託
    internal class MailManager
    {
        public event EventHandler<NewEventArgs> NewMail;
        //收到信息
        public void Go(string value)
        {
            OnMail(new NewEventArgs(value));
        }
        protected virtual void OnMail(NewEventArgs e)
        {

        EventHandler<NewEventArgs> temp = Volatile.Read(ref NewMail);
        if (temp != null)
        temp(this, e);

        }
    }

    //回調
    internal class Fax {
        public Fax(MailManager n)
        {
            n.NewMail += delegate (object sender, NewEventArgs e) { Console.WriteLine(e.str_name); };
        }
    }


MailManager mail = new MailManager();
            Fax f = new Fax(mail);
            mail.Go("接收到新消息");

event關鍵字表明事件不能從其餘類執行或者添加,只能在當前類進行操做,至關於委託的private。

EventHandler<T> 表示將處理不包含事件數據的事件的方法。T 數據類型,傳遞事件源與數據對象。

Volatitle.Read 原子性讀取,強迫在調用發生的時候進行讀取。避免多線程操做會刪除此數據。

泛型:是CLR提供的一種特殊機制,支持另外一種形式的代碼重用,即算法重用。

泛型的優點以下:

源代碼保護:使用泛型算法的開發人員不須要訪問算法的源代碼。

類型安全:將泛型算法應用於一個具體類型時,只有與指定數據類型兼容的對象才能用算法。

更清晰的代碼:在使用的時候,減小了使用強制轉換的操做。

更佳的性能:減小裝箱操做,將以值類型來進行操做。

建議使用泛型集合類,緣由以下

使用非泛型集合類,沒法獲得類型安全保證。

泛型集合類,性能更加。

常見的泛型

Dictionary<TKey,TValue>:鍵值對集合

ConcurrentDictionary<TKey,TValue>:線程安全的鍵值對

(非泛型)HashTable:基於哈希表示的鍵值對集合,其中key-value均爲object。

HashSet<T>:值集合,基於哈希的查詢速度快,不容許重複,不能經過下標檢索

List<T>:強類型集合

ConcurrentBag<T>:線程安全的無序集合

Queue<T>:先進先出集合,隊列

ConcurrentQueue<T>:線程安全的先進先出集合

Stack<T>:後進先出集合,棧

ConcurrentStack<T>:線程安全的後進先出集合

委託的每一個泛型類型參數均可以標記爲協變量或逆變量。泛型類型參數能夠是如下任何一種。

不變量:類型參數不能修改。

逆變量:類型參數能夠從一個類更改成它的某個派生類,由大變小。使用in關鍵字標記。

協變量:類型能夠從一個類更改成它的某個基類,由小變大。使用out關鍵字標記。

例如:public delegate TResult Func<in T,out TResult>(T arg);

Func<Object, ArgumentException> fn = null;
            Func<String,Exception> fn1 = null;
            fn1 += fn;

fn1 的 逆變讓String能夠變成 Object,協變讓 Exception 能夠變成ArgumentException。

泛型類型約束

CLR支持稱爲約束的機制,做用是限制能指定成泛型實參的類型數量。使用where 關鍵字來定義。 

主要約束:類型參數能夠指定零個或一個主要約束。主要約束能夠是表明非密封類的一個引用類型。例如:Stream

還有兩個特殊的主要約束:class 和 struct。

次要約束:類型參數能夠指定零個或多個次要約束。次要約束表明接口類型。例如:IEnumerable

還有一種特殊的類型參數約束:容許指定類型實參看成約束。例如

public static List<TBase> ConverIList<T, TBase>(IList<T> list) where T : TBase{}

ConverIList<String, Object>();

其中TBase是T的類型約束

構造器約束:類型參數能夠指定零個或一個構造器約束,必須實現公共無參構造器的非抽象類型。new()

泛型變量沒法與其餘類型進行顯示轉換。必須使用 as 操做符,來進行轉換。

public static void Go<T>(T obj)
        {
            string s = (string)obj; //報錯:沒法轉換
            string ss = obj as string; //正確
        }

泛型類型沒法設置爲Null。由於有可能包含值類型,設爲null是不可能的。

但可使用 default 關鍵字,來設置成默認類型。若是是引用類型則設置成null,若是是值類型則設置成0.

迭代過濾器:yield

能夠在迭代過程當中,根據條件返回篩選事後的值。顧名思義,迭代只能是在foreach裏面使用。使用yield關鍵字,能夠返回一個篩選事後的集合。例如:我使用一個擴展方法,來進行迭代過濾。

internal static class SampleClass
    {
        public static IEnumerable<char> Filter(this IEnumerable<char> enumList)
        {
            foreach (char item in enumList)
            {
                if (item != char.Parse("A"))
                {
                    yield return item;
                }
            }
        }
    }


 foreach (var item in "ABCDABCABD".Filter())
            {
                Console.Write(item);
            }

接口

由於CLR不支持多繼承,因此提供接口功能來實現 縮水版 的多繼承關係。一個類能夠繼承多個接口。

接口就是對一組方法進行統一命名,方法不會實現。當類繼承接口的時候,必須實現對應的方法。

使用 interface 關鍵字來定義接口。約定接口類型名稱以大寫字母I開頭。

編譯器要求將實現接口方法標記爲public,CLR要求將接口方法標記爲virtual。派生類能夠重寫。

不顯示標記virtual,會自動標記爲virtual和sealed。派生類不能重寫

派生類不能重寫sealed的接口方法。但派生類能夠繼承同一個接口,並提供對應的實現。在對象上調用接口方法時,會調用對應的實現。

public sealed class Program
    {
        static void Main(string[] args)
        {
            Base b = new Derived();
            b.Dispose(); //Base's Dispose
            ((IDisposable)b).Dispose(); //Derived's Dispose

            Console.ReadLine();
        }

        internal class Base : IDisposable {

            public void Dispose()
            {
                Console.WriteLine("Base's Dispose");
            }
        }

        internal class Derived : Base, IDisposable {
            
            new public void Dispose()
            {
                Console.WriteLine("Derived's Dispose");
            }
        }
    }

Base 和 Derved,都繼承了IDisposable接口,並實現了不一樣的方法。當將b顯示轉換爲IDisposable對象的時候,就會調用Derived對應的方法。

new 關鍵字可重寫方法,並替換原先的接口方法。

基類仍是接口?:屬於 和 能 關係,若是基類和派生類創建不起屬於的關係,就不用基類而用接口。

Net Framework中,字符老是表示成 16位的 Unicode代碼。每一個字符都是 System.Char 結構。

String表明一個不可變的順序字符集,直接派生Object,是引用類型。

許多編程語言都講String視爲基元類型,不容許使用new關鍵字從構造對象。必須使用簡化語法。

轉移機制,不建議直接寫 \r \n 這種。相反提供平臺敏感的NewLine屬性,Environment.NewLine。在任何平臺上都能工做。

逐字字符串:採用這種方式,全部字符都被視爲字符串的一部分。須要用 @ 來聲明同一個字符串。string file @"c:\windows\Notepad.exe";

字符串不可變。字符串一經建立便不能更改,不能變長,變短或修改字符。但容許在字符串上執行各類操做,而不實際地更改字符串。

因爲String類型表明不可變字符串,對字符串的每個操做,都會分配堆,會在GC中進行回收。FCL提供了StringBuilder類型,對字符串進行高效動態處理。

並能夠返回處理好的String對象。StringBuilder就是建立String對象的特殊構造器。

StringBuilder 是可變字符串。大多數成員均可以改變字符串內容。並不會分配堆。

ToString 容許三種格式化輸出:G常規,D十進制,G十六進制。

枚舉類型定義了一組「符號名稱/值」配對。使用enum來標識。例如

public enum Color { 
            White, //0
            Red //1
        }

枚舉類型的優點

更容易編寫、閱讀、維護。有了枚舉類型,符號名稱可在代碼中隨便使用。不用擔憂含義。一旦對應值改變,代碼也能夠簡單編譯,不須要修改源碼。

枚舉類型是強類型。如傳遞額外內容,編譯器會報錯。

枚舉類型是從System.ValueType派生,因此是值類型。枚舉類型不能定義方法、屬性、事件。不過可用擴展方法添加枚舉類型。

IsDefined能夠判斷數值是否包含於枚舉類型中

Console.WriteLine(Enum.IsDefined(typeof(Color),"Red"));//True

位標誌定義了一組集合。使用enum來標識,並添加 Flags 特性。

public sealed class Program
    {
        static void Main(string[] args)
        {
            Color col = Color.White | Color.Red; 
            Console.WriteLine(col.ToString());//White, Yello

            Console.ReadLine();
        }

        [Flags]
        public enum Color { 
            White = 0x001,
            Red,
            Yello = 0x002
        }
    }

若是數值沒有對應的符號,ToString方法會監測Flags特性。若是有則會將它視爲一組標誌。若是沒有則會返回數值。

Flags 賦值通常都是採用十六進制,間隔爲2的N次冪。

數組:容許將多個數據項做爲集合來處理的機制。CLR支持一維、多維、交叉數組,數組均是引用類型。

Double[,] myDoubles = new Double[10, 20]; //二維數組
String[, ,] myStrings = new String[5, 3, 10]; //三維數組
//交叉數組
String[][] myPloygons = new String[3][];
myPloygons[0] = new String[10];
myPloygons[1] = new String[5];

數組初始化

string[] name = { "chenxy", "ccc" };
var names = new[] { new { Name = "Chenxy" }, new { Name = "ccc" } };

數組拷貝

Int32[] ildim = { 1, 2, 3, 4, 5 };
Object[] obldim = new Object[ildim.Length];
Array.Copy(ildim, obldim, ildim.Length);

全部數組都隱式實現IEnumerable,ICollection,IList。

委託

Net Framework提供了稱爲 委託 的類型安全機制,用於提供回調函數的機制。委託使用delegate來標識。

public sealed class Program
    {
        public delegate void Feeback(int value);

        static void Main(string[] args)
        {
            Feeback f1 = new Feeback(o => Console.WriteLine("第一個回調{0}", o));
            Feeback f2 = new Feeback(o => Console.WriteLine("第二個回調{0}", o));
            f1 += f2;
            f1(123);

            Console.ReadLine();
        }
    }

全部操做都是類型安全的。將方法綁定到委託時,都容許引用類型的協變性和逆變性。值類型不容許。

定義委託類型,能夠加載參數和返回值類型同樣的方法。new Feedback 使用Lambda 表示式,進行匿名方法。

設置好委託鏈,進行調用便可實現回調函數。

關聯兩個委託可使用方法:Delegate.Combine、Delegate.Remove 或者 +=、-+操做符。委託會重載這兩個操做符來實現委託鏈。

委託定義不要太多,由於委託類型的特殊性,若是參數同樣的話,不須要定義太多的委託。目前支持的泛型委託有兩種。

Action 無返回值可設置參數,目前支持16個泛型參數。

Func 有返回值,可設置參數,目前支持16個泛型參數。

public sealed class Program
    {
        static void Main(string[] args)
        {
            //Lambda表達式
            Action<string, int> ac1 = (o, t) => Console.WriteLine("測試字符串{0},值{1}", o, t);
            //Linq表達式
            Action<string, int> ac2 = delegate(string o, int t) { Console.WriteLine("測試字符串{0},值{1}", o, t); };
            //方法
            Action<string, int> ac3 = Method1;
            ac1("陳星宇", 1993);
            ac2("陳星宇", 1993);
            ac3("陳星宇", 1993);

            Func<string, int, string> fc1 = delegate(string o, int t) { string result = string.Format("測試Func,字符串 {0},值 {1}", o, t); return result; };
            Console.WriteLine(fc1("陳星宇", 1993));

            Console.ReadLine();
        }

        public static void Method1(string o, int t)
        {
            Console.WriteLine("測試字符串{0},值{1}", o, t);
        }
    }

Expression<T> Lambda表達式樹

表達式樹是Lambda表達式在內存中的數據表示形式。它使Lambda表達式的結構變得更加透明而明確的數據結構。

在與表達式目錄樹進行交互時,就如同和其餘數據類型交互同樣。

public void Method1(Expression<Func<int, bool>> tDelegate)
        {
            Console.WriteLine(tDelegate.Compile()(3));
        }

s.Method1(i => i < 5);

表達式樹建立須要使用Expression類,這個類包含建立特定類型表達式樹節點的靜態工廠方法。

參數變量的ParameterExpression、方法調用的MethodCallExpression

http://www.cnblogs.com/wolf-sun/p/4222473.html

http://www.cnblogs.com/wolf-sun/p/4230226.html

http://www.cnblogs.com/wolf-sun/p/4234747.html

¥ 

關於C#委託的簡化語法

1.不須要構造委託對象,C#編譯器能夠本身進行推斷方法類型。不須要構造委託對象。例如

public static void Callback()
        {
            ThreadPool.QueueUserWorkItem(SomeAsync, 5);
            //public delegate void WaitCallback(object state)
        }

        public static void SomeAsync(Object o)
        {
            Console.WriteLine(o);
        }

QueueUserWorkItem 定義接受的委託對象 是 WaitCallback,能夠不用new 一個結構,直接傳相同類型的方法便可。

2.不須要定義回調方法,容許之內聯的方式寫回調方法的代碼,沒必要在他本身的方法中寫。

public static void Callback()
        {
            ThreadPool.QueueUserWorkItem(obj => Console.WriteLine(obj), 5);
            //public delegate void WaitCallback(object state)
        }

如不須要參數的Lambda表達式能夠寫爲:()=>Console.writle(); 也能夠定義out/ref參數,(out obj)=>Console...

3.局部變量不須要手動包裝到類中便可傳回調方法

當委託在單獨的一個方法的時候,計算出的變量值傳回調用方法是很麻煩的。可是Lambda表達式則能夠直接賦值。

public static void Callback()
        {
            int numToDo = 10;
            Int32[] squares = new Int32[numToDo];
            AutoResetEvent done = new AutoResetEvent(false);
            for (int i = 0; i < squares.Length; i++)
            {
                ThreadPool.QueueUserWorkItem(obj => {
                    int num = (int)obj;
                    squares[num] = num * num;
                    if (Interlocked.Decrement(ref numToDo) == 0)
                        done.Set();
                }, i);
            }
            done.WaitOne();
            for (int n = 0; n < squares.Length; n++)
            {
                Console.WriteLine("Index {0},Square {1}",n,squares[n]);
            }
        }

能夠在委託中,給squares 賦值。若是是單獨的方法,則會很麻煩。

委託和反射

針對不知道回調方法須要多少個參數的這種狀況,能夠進行動態委託類型的建立。

internal delegate Object Two(Int32 n1, Int32 n2);
    public sealed class Program
    {
        static void Main(string[] args)
        {
            string[] arg = { "ConsoleApplication1.Two", "Add", "123", "321" };
            //判斷委託是否是在類型中存在
            Type delType = Type.GetType(arg[0]);
            if (delType == null)
                return;
            //定義一個委託,後期動態建立這個委託
            Delegate d;
            //獲取類型中的方法。
            MethodInfo mi = typeof(Program).GetTypeInfo().GetDeclaredMethod(arg[1]);
            //根據方法建立對應的委託對象
            d = mi.CreateDelegate(delType);
            //建立一個對象容器
            Object[] callbackArgs = new Object[arg.Length - 2];
            for (int a = 2; a < arg.Length; a++)
            {
                callbackArgs[a - 2] = int.Parse(arg[a]);
            }
            //動態調用方法
            Object result = d.DynamicInvoke(callbackArgs);
            Console.WriteLine(result);

            Console.ReadLine();
        }


        private static Object Add(Int32 n1, Int32 n2)
        {
            return n1 + n2;
        }
    }

使用Delegate 用來接受建立的動態委託,根據方法的CreateDelegate來進行建立。最後使用委託的DynamicInvoke來動態調用。

定製特性

特性:能夠宣告式的爲本身的代碼構造添加註解來實現特殊功能。容許爲每個元數據表記錄定義和應用信息。

C#容許將特性應用以下的目標元素。

[assembly: SomeAttr] //應用程序集
[module:SomeAttr] //應用模塊
namespace DemoArray
{
    [type:SomeAttr] //應用類型
    internal sealed class SomeType<[typevar:SomeAttr] T> //應用泛型類型變量
    {
        [field:SomeAttr] //應用字段
        public Int32 SomeField = 0;

        [return:SomeAttr] //應用返回值
        [method:SomeAttr] //應用方法
        public Int32 SomeMethod(
            [param:SomeAttr] //應用參數
            Int32 SomeParam) {
            return SomeParam;
        }

        [property:SomeAttr] //應用屬性
        public string SomeProp
        {
            [method:SomeAttr] //應用get訪問器
            get { return null; }
        }

        [event:SomeAttr] //應用事件
        [field:SomeAttr] //應用編譯器生成的字段
        [method:SomeAttr] //應用編譯器生成的add & remove方法
        public event EventHandler SomeEvent;
    }

    internal class SomeAttrAttribute : Attribute
    {
    }
}

特性是一個類型實例,必須直接或間接的從公共抽象類System.Attribute派生。特性必須有公共構造器,纔可以建立。下面這個例子

[DllImport("Kernel", CharSet = CharSet.Auto, SetLastError = true)]

參數Kernel,是構造器參數。在特性中稱爲:定位參數。並且是強制性的,應用特性的時候,必須填寫指定參數。

CharSet,SetLastError 用於設置字段或屬性的參數。在特性中稱爲:命名參數。這種參數是可選的。

自定義特性

[AttributeUsage(AttributeTargets.Enum, Inherited = false, AllowMultiple = true)]
    sealed class FlagsAttribute : Attribute
    {
        public FlagsAttribute()
        {
        }
    }

AttributeUsage 可利用它告知編譯器定製特性的合法應用範圍。例子中,這個特性只能用於Enum。

Inherited 指特性在應用於基類的時候,是否同時應用於派生類和重寫的方法。

AllowMultiple 是否能爲一個實例,指定多個對象。

檢測定製特性類

System.Reflection 提供了三個靜態擴展方法來獲取與目標相關的特性:IsDefined,GetCustomAttributes,GetCustomAttribute

若是隻想判斷目標是否應用一個特性,應調用IsDefined。由於它比其餘兩個更高效。不會構造特性對象,不會調用構造器,也不會設置字段和屬性。

要構造特性對象,必須調用GetCustomAttributes 或 GetCustomAttribute方法。每次調用該方法都會構造特性對象並返回。

IsDefined:若是至少有一個指定的特性實例與目標關聯,就返回true。由於不構造特性對象,效率很高。

GetCustomAttributes:返回應用目標的指定特性對象集合。如沒有就返回空集合。該方法經常使用於AllowMultiple設爲true的特性。

GetCustomAttribute:返回應用於目標的指定特性類的實例。若是沒有就返回Null。若是定義多個特性會拋出異常。該方法一般用於AllowMultiple設爲false的特性。

將一個類傳給這些方法的時候,會監測是否應用了指定的特性類或派生類。若是隻是想搜索一個具體的特性類,能夠考慮將本身的特性類 sealed。

下面作一個獲取特性的例子

[Serializable]
    public sealed class Program
    {
        static void Main(string[] args)
        {
            ShowAttributes(typeof(Program));

            Console.ReadLine();
        }

        /// <summary>
        /// 顯示特性
        /// </summary>
        /// <param name="attributeTarget">獲取成員屬性信息。擴展方法之一</param>
        private static void ShowAttributes(MemberInfo attributeTarget)
        {
            var attributes = attributeTarget.GetCustomAttributes<Attribute>();
       bool result = attributeTarget.IsDefined(typeof(SerializableAttribute), false); Console.WriteLine(
"特性應用 {0}:{1}",attributeTarget.Name,(attributes.Count() == 0 ? "None" : String.Empty)); foreach (Attribute attribtue in attributes) { if (attribtue is SerializableAttribute) { Console.WriteLine("Serializable TypeId={0}",((SerializableAttribute)attribtue).TypeId); } } Console.WriteLine(); } }

分析一下:首先GetCustomAttributes擴展方法能夠用於。Assembly 程序集,MemberInfo 類型成員,Module 模塊成員,ParameteInfo 方法成員。

咱們是設置的特性是類型成員,因此ShowAttribtues接受MemberInfo。接下來就顯而易見了,遍歷集合而後is判斷轉換,最後顯示轉換拿屬性。搞定!

條件特性類:設置特性只會在指定條件下執行。避免因特性過多形成系統更大,損害系統性能。System.Diagnostics.ConditionalAttribtue

[Conditional("TEST")] #define TEST

若是發現向目標添加此特性,只會在此定義TEST符號的前提下,編譯器纔會運行特性。#define是C#預處理指令,必須寫在頁面最上方。

C#預處理指令,能夠在編譯代碼的時候進行預處理操做。包括定義環境,IF判斷,拋出異常等等。C# 預處理指令

也能夠經過發佈選項,來自動轉換到對應的條件下。參考資料:C#可否獲取發佈選項是Debug仍是Release?

項目生成中有選項 ,定義 DEBUG 常量。而後不須要#define,直接#if就能夠了。

'#if DEBUG

'#endif

可空類型:使值類型能夠設爲null,使用?操做符。例如:Int32? a = null;

空接合操做符:使用??操做符。獲取兩個操做數,若是左邊不爲null,就返回這個操做數的值。不然返回右邊操做數的值。能夠用於引用類型和可空類型。

Int32 b = null ;  Int32 x = b ?? 123;

異常和狀態管理

Net Framework異常處理機制,是使用Microsoft Windows提供的結構化異常處理(Structured Exception Handling,SEH)機制構建的。

先經過一個例子,來看看異常處理機制的寫法。

private void SomeMethod()
        {
            try
            {
                // 須要檢測異常的代碼
            }
            catch (InvalidOperationException)
            {
                // 從InvalidOperationException恢復的代碼放在這裏
            }
            catch (IOException)
            {
                // 從IOException恢復的代碼放在這裏
            }
            catch
            {
                //除了上面這兩個異常,其餘全部異常恢復代碼
                //須要從新拋出異常
                throw;
            }
            finally
            {
                //此處代碼老是執行的,無論有沒有異常
            }
        }

try:若是代碼須要執行通常性的資源清理操做,就放到try塊中。try 必須和finally 或 catch 配合使用,不容許單獨出現。

catch:包含響應一個異常須要執行的代碼。一個try能夠關聯多個catch塊。圓括號中的表達式稱爲:捕捉類型。必須是從Exception派生的類型。

finally:包含的是保證會執行的代碼。通常在此模塊中執行資源清理操做。

throw:能夠拋出異常位置。當捕捉到異常時,會記錄捕捉位置。當throw拋出異常時,CLR會重置異常起點。單獨使用throw關鍵字,會從新拋出相同的錯誤。

若是方法沒法完成任務的時候,就應拋出一個異常。拋出異常須要考慮兩個問題:

1.是拋出什麼Exception派生類型。應選擇一個有意義的類型。可直接使用FCL定義好的類型,也能夠本身定義類型,可是必須從Exception中派生。

2.向異常類型傳遞什麼字符串信息。拋出異常的時候,應包含一條字符串信息,說明爲何沒法完成任務。

異常使用提供的設計規範

1.善用finally塊:先用finally清理已經成功啓動的操做,再返回至調用者或者執行finally快以後的代碼。使用lock,using,foreach會自動生成try/finally塊。

2.不要什麼都捕捉:捕捉異常代表你預見到該異常,理解它爲何發生,並知道如何處理它。因此絕對不容許捕捉 Exception 全部異常。

   若是吞噬異常而不從新拋出,應用程序會不知道已經出錯,仍是會繼續運行,形成不可預測的結果和潛在的安全隱患。

3.得體的從異常中恢復:因爲能預料到這些異常,因此能夠寫一些代碼,容許應用程序從異常中得體的恢復並繼續運行。例如:

public string CalculateSpreadsheetCell(Int32 row, Int32 column)
        {
            string result;
            try
            {
                result = "";//計算電子表格單元格中的值
            }
            catch (DivideByZeroException)
            {
                result = "除以零的值不能顯示";
            }
            catch (OverflowException)
            {
                result = "計算值過大不能顯示";
            }
            return result;
        }

4.發生不可恢復的異常時回滾部分完成的操做:如出現異常,應用程序應回滾已部分完成的操做,將文件恢復爲任何對象序列化以前的狀態。例如:

public void SerializeObject(FileStream fs, IFormatter formatter, Object rootObj)
        {
            Int64 beforeSerialization = fs.Position; //保存當前流的位置
            try
            {
                formatter.Serialize(fs, rootObj); //將對象圖序列化到文件中
            }
            catch //捕捉全部異常,只有失敗的時候纔對流進行重置
            {
                fs.Position = beforeSerialization;//任何事情出錯,就將文件恢復到一個有效狀態。
                fs.SetLength(fs.Position); //截斷文件
                throw; //拋出相同異常
            }
        }

5.隱藏實現細節來維繫協定:有時候須要捕捉異常並拋出不一樣的異常。拋出的異常,應該是具體異常。

未處理異常

CLR調用棧中向上查找與拋出對象相符的catch塊。若是沒有匹配的話,就發生一個 未處理異常。CLR檢測到進程中的任何未處理異常,都會終止進程。

託管堆和垃圾回收

只要寫類型安全的代碼,應用程序就不可能會出現內存被破壞的狀況。

大多數類型都無需資源清理,垃圾回收器會自動釋放內存。若是須要儘快清理資源,能夠調用Dispose方法。

CLR要求全部對象都從託管堆分配,進程初始化時,CLR劃出一個地址空間區域做爲託管堆。一個區域被非垃圾對象填滿後,CLR會分配更多的區域。

這個過程一直重複,直到整個進程地址空間被佔滿。32位進程最多分配1.5GB,64位進程最多分配8TB

new操做符會致使CLR執行如下步驟:

1.計算類型的字段所需的字節數

2.加上對象的開銷所需的字節數。

3.CLR檢測區域中是否有分配對象所需的字節數。分配對象只需在指針上加一個值,速度很是快。

垃圾回收算法(GC)

當調用new關鍵字的時候,若是沒有足夠的地址空間來分配該對象。發現空間不夠,CLR就執行垃圾回收。

CLR使用引用跟蹤算法,此算法只關心引用類型的變量,由於只有這種變量才能引用堆上的對象。值類型變量直接包含值類型實例。

根:咱們將引用類型變量稱爲根。引用類型變量,包括類的靜態和實例字段,或者方法的參數和局部變量。

開始GC的時候,首先會暫停進程中的全部線程。防止檢查期間訪問對象並更改其狀態。

而後CLR進入GC的標記階段。這個階段CLR遍歷堆中的全部對象,將同步快索引字段的一位設爲0。代表全部對象都應該刪除。

而後CLR檢查全部活動根,查看他們引用了那些對象。若是一個根包含null,CLR忽略這個跟並繼續檢查下個根。

任何跟若是引用了堆上的對象,CLR會標記那個對象設爲1。

檢查完畢後,已標記的對象不能被垃圾回收,由於至少有一個根在引用它們。因此這種對象是可達的,由於應用程序中不存在使對象能被再次訪問的根。

CLR知道標記之後,就會進行壓縮階段。會壓縮全部倖存下來的對象,使它們佔用連續的內存空間。防止空間碎片化的問題。

CLR的GC是基於代的垃圾回收器,它對你的代碼作出如下幾點假設

1.對象越新,生存期越短。

2.對象越老,生存期越長。

3.回收堆的一部分,速度快於回收整個堆。

託管堆在初始化時不包含對象。添加到堆的對象稱爲第0代對象。均爲新對象,垃圾回收器從未檢查過它們。

CLR初始化第0代對象選擇一個預算容量。若是分配一個新對象形成第0代超過預算,就必須啓動一次垃圾回收。

垃圾回收事後,第0代就不包含任何對象了。在垃圾回收機制中存活下來的根,就跑到第一代對象裏面了。當再次回收的時候,依然仍是回收第0代對象。

垃圾回收過程當中,會檢查第一代的預算容量。若是第一代小於預算容量,就回收第0代。

只有第一代超出預算,纔會檢查第一代中的對象。若是第一代對象滿了,就會自動劃分到第二代。一共只有三代,0 1 2

垃圾回收觸發條件

檢查第0代超過預算時,觸發一次GC。

顯示調用System.GC 的 Collect方法。

Windows報告低內存狀況。

CLR正在卸載AppDomain。

CLR正在關閉。

大對象:超過85000字節的對象,稱爲大對象。大對象分配不一樣的地址,而且GC不會進行壓縮,大對象老是第2代。

特殊清理類型

CLR提供稱爲終結的機制,也稱爲析構方法。容許對象在被斷定爲垃圾以後,在對象內存被回收以前執行一些代碼。任何包裝本機資源的類型都支持終結。

internal sealed class SomeType {
        ~SomeType() { 
            //這裏寫執行代碼
        }
    }

CLR寄宿和AppDomain

寄宿:使任何應用程序都能利用CLR的功能。使現有的應用程序至少能部分使用託管代碼編寫。託管代碼就是託管模塊,能夠轉換爲IL。

Microsoft爲CLR定義了一個標準的COM接口,併爲該接口和COM服務器分配GUID。安裝Net Framework時,會在Windows註冊表中註冊。

AppDomain:CLR COM服務器初始化時會建立一個AppDomain。他是一組邏輯容器(應用程序域)。也是默認的AppDomain只有在Windows進程終止時纔會被銷燬。

除了默認的AppDomain還能夠加載額外的AppDomain。具體功能以下

一個AppDomain的代碼不能直接方位另外一個AppDomain的代碼建立的對象。只能使用「按引用封送」

AppDomain能夠卸載,同時卸載包含的程序集

AppDomain能夠單獨保護,能夠保證這些代碼不會被破壞。

AppDomain能夠單獨配置,關聯一組配置設置。

程序集加載和反射

程序集加載:Assembly.Load();致使CLR向程序集應用一個版本綁定重定向策略,並在GAC(全局程序集緩存)中查找程序集。若是沒找到,就去應用程序的基目錄、私有目錄去找。

若是傳遞的是弱命名程序集,就不會執行重定向策略。若是Load找到指定的程序集,就會返回Assembly對象的引用。

Assembly.LoadFrom 容許傳遞一個URL做爲實參。若是傳遞的是一個Internet位置,CLR會下載文件。

反射的性能

反射是很是強大的機制,與容許在運行時發現並使用編譯時還不瞭解的類型及其成員。可是也有兩個缺點:

反射形成編譯時沒法保證類型安全。因爲反射嚴重依賴字符串,因此會喪失編譯時的類型安全。好比:Type.GetType("int") 反射不認識基元類型,只能夠寫System.Int32

反射速度慢。使用反射時,類型及其成員的名稱在編譯時未知。若是用字符串的話,反射機制會不停的執行字符串搜索。

發現程序集中定義的類型

private static void Marshalling()
        {
            Assembly a = Assembly.Load(typeof(Program).Assembly.FullName);
            foreach (Type item in a.ExportedTypes)
            {
                Console.WriteLine(item.FullName);
            }
        }

構造類型實例

得到Type對象引用後,能夠遠程構造該類型的實例。FCL提供如下幾個方法

1.System.Activator 的 CreateInstance方法:傳遞一個Type對象引用,也能夠傳遞標識類型的String。返回新對象的引用。

2.System.Activator 的 CreateInstanceFrom方法:必須經過字符串參數來指定類型及其程序集。返回對象引用。

3.System.AppDomain:容許指定那個在AppDomain中構造對象,不過不是託管代碼不推薦使用。

4.System.Reflection.ConstructorInfo 的 Invoke實例方法:使用一個Type對象引用,能夠綁定到一個特定的構造器。能夠調用Invoke方法,返回新對象的引用。

如何查詢類型的成員並顯示成員信息

字段、構造器、方法、屬性、事件和嵌套類型均可以定義成類型的成員。FCL包含抽象基類SYstem.Reflection.MemberInfo 封裝了全部類型成員都通用的一組屬性。

對每一個類型調用DeclaredMembers屬性返回派生對象構成的集合。而後顯示對應的成員信息。

public sealed class Program
    {
        static void Main(string[] args)
        {
            Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
            foreach (Assembly a in assemblies)
            {
                Show(0, "Assembly:{0}", a);
                foreach (Type t in a.ExportedTypes)
                {
                    Show(1, "Type:{0}", t);
                    foreach (MemberInfo mi in t.GetTypeInfo().DeclaredMembers)
                    {
                        string typeName = string.Empty;
                        if (mi is Type)
                            typeName = "Type";
                        Show(2,"{0}:{1}",typeName,mi);
                    }
                }
            }
            Console.ReadLine();
        }

        private static void Show(Int32 indent, String format, params Object[] args)
        {
            Console.WriteLine(new String(' ', 3 * indent) + format, args);
        }
    }

調用類型的成員

發現類型定義的成員後可調用它們。不一樣的類型成員調用方法也不一致。

FieldInfo  調用GetValue獲取字段的值  調用SetValue設置字段的值

ConstructorInfo  調用Invoke構造類型的實例並調用構造器

MethodInfo  調用Invoke來調用類型的方法

PropertyInfo  調用GetValue來調用的屬性的get訪問器方法  調用SetValue來調用屬性的Set訪問器方法

EventInfo  調用AddEventHandler來調用事件的add訪問器方法  調用RemoveEventHandler來調用事件的remove訪問器方法

看一個總體的例子

public sealed class Program
    {
        private Int32 m_someField;
        public override string ToString()
        {
            return m_someField.ToString();
        }
        public Int32 SomeProp
        {
            get { return m_someField; }
            set
            {
                m_someField = value;
            }
        }
        public static event EventHandler SomeEvent;
        public Program(ref Int32 x)
        {
            x *= 2;
        }
        static void Main(string[] args)
        {
            #region 反射構造函數
            Type ctorArg = Type.GetType("System.Int32&"); //ref 的 類型
            ConstructorInfo ctor = typeof(Program).GetTypeInfo().DeclaredConstructors.First(c => c.GetParameters()[0].ParameterType == ctorArg);
            Object[] arg = new Object[] { 12 };
            Object obj = ctor.Invoke(arg);
            Console.WriteLine("調用構造函數,返回的值:{0}", arg[0]);
            #endregion

            #region 讀取字段
            FieldInfo fi = obj.GetType().GetTypeInfo().GetDeclaredField("m_someField");
            fi.SetValue(obj, 33);
            Console.WriteLine("調用字段 {0}", fi.GetValue(obj));
            #endregion

            #region 調用方法
            MethodInfo mi = obj.GetType().GetTypeInfo().GetDeclaredMethod("ToString");
            string s = mi.Invoke(obj, null) as string;
            Console.WriteLine("ToString: {0}", s);
            #endregion

            #region 讀寫屬性
            PropertyInfo pi = obj.GetType().GetTypeInfo().GetDeclaredProperty("SomeProp");
            pi.SetValue(obj, 2, null);
            Console.WriteLine("屬性:{0}", pi.GetValue(obj, null));
            #endregion

            #region 事件添加或刪除委託
            EventInfo ei = obj.GetType().GetTypeInfo().GetDeclaredEvent("SomeEvent");
            EventHandler en = delegate(Object sender, EventArgs e) { Console.WriteLine("我是事件"); };
            ei.AddEventHandler(obj, en);
            SomeEvent("", null);
            #endregion

            Console.ReadLine();
        }
    }

轉換成GetTypeInfo,而後在進行獲取對應的成員。

運行時序列化

序列化是將對象或對象圖轉換成字節流的過程。反序列化是將字節流轉換回對象圖的過程。

序列化的優點

1.應用程序狀態能夠輕鬆保存到磁盤文件或數據庫中,並在應用程序下次運行時恢復。

2.一組對象可輕鬆複製到系統的剪貼板,再粘貼回一個或另外一個應用程序。

3.一組對象可克隆並放到一邊做爲「備份」;與此同時,用戶操做一組「主」對象。

4.一組對象可輕鬆地經過網絡發送給另外一臺機器上運行的過程。

5.能夠方便的進行加密和壓縮處理。

序列化小例子

public sealed class Program
    {
        static void Main(string[] args)
        {
            var objectGraph = new List<string> { "A", "B", "C" };
            Stream stream = SerializeToMeroy(objectGraph);

            stream.Position = 0;
            objectGraph = null;

            objectGraph = DeserializeFromMeroy(stream) as List<string>;
            foreach (var item in objectGraph)
            {
                Console.WriteLine(item);
            }

            Console.ReadLine();
        }

        private static MemoryStream SerializeToMeroy(Object objectGraph)
        {
            MemoryStream stream = new MemoryStream();
            BinaryFormatter formatter = new BinaryFormatter();
            formatter.Serialize(stream, objectGraph);
            return stream;
        }

        private static Object DeserializeFromMeroy(Stream stream)
        {
            BinaryFormatter formatter = new BinaryFormatter();
            return formatter.Deserialize(stream);
        }
    }

FCL提供兩個格式化器:BinaryFormatter、SoapFormatter。序列化對象圖只須要調用格式化器的Serialize方法,並向他們傳遞兩樣東西。

對流對象的引用,以及對想要序列化的對象圖的引用。

流對象標識了序列化好的字節應該放到哪裏。他是System.IO.Stream抽象基類派生的任何類型的對象。例如:MemoryStream,FileStream,NetworkStream

對象引用能夠是任何東西,能夠引用集合,而且能夠嵌套引用。

能夠經過序列化機制,來進行對象的深拷貝。

private static Object DeepClone(Object original)
        {
            using (MemoryStream stream = new MemoryStream())
            {
                BinaryFormatter formatter = new BinaryFormatter();
                formatter.Context = new StreamingContext(StreamingContextStates.Clone);
                formatter.Serialize(stream, original);
                stream.Position = 0;
                return formatter.Deserialize(stream);
            }
        }

深拷貝與淺拷貝:他倆的區別只是引用類型的處理方式不一樣。

淺拷貝:只複製對象的基本類型,對象類型,仍屬於原來的引用

深拷貝:不緊複製對象的基本類,同時也複製原對象中的對象.就是說徹底是新對象產生的

若是須要序列化,必須添加SerializeAttribute特性。這個特性只能應用於引用類型、值類型、枚舉類型、委託類型。其中枚舉和委託不用顯示定義特性。

SerializeAttribute特性,不會被派生類繼承。因此父類標記序列化,子類不適用。若是子類序列化,父類沒有序列化,也不能進行。

通常建議將大多數類型都設置可序列化,但由於序列化會讀取私有對象。若是類型包含敏感或安全數據,就不該使類型變得可序列化。

NonSerializedAttribute 能夠指出類型中不該序列化的字段,該特性只能應用於類型中的字段,而且會被派生類繼承。

OnDeserializedAttribute 每次反序列化後,都會調用標記的方法。

OnDeserializingAttribtue 每次反序列化時,都會調用標記的方法。

OnSerializedAttribute 每次序列化後,都會調用標記的方法。

OnSerializingAttribute 每次序列化時,都會調用標記的方法。

使用這四個方法的時候,必須獲取一個StreamingContext 流上下文。並返回void,方法名隨意。應將方法聲明private,以避免被普通方法調用。

OptionalFieldAttribute 新增字段使用此特性,能夠繞過反序列化時因字段數量不一樣的異常。

StreamingContext 流上下文

一個對象可能須要知道它要在什麼地方反序列化。就可使用流上下文,結構包含一組位標誌、一個對象引用。位標誌指明要序列化/反序列化的來源。

BinaryFormatter和SoapFormatter定義了Streaming類型的屬性Context。能夠設置額外引用對象,例如深拷貝,就是使用流上下文來通知序列化器的。

使用線程的理由

1.可響應性:Windows爲每一個進程提供它本身的線程,確保發生死循環的應用程序不會影響到其餘應用程序。在圖形管理界面中,能夠將工做交給一個線程,又不影響UI。

2.性能:因爲Windows每一個CPU調度一個線程,多個CPU會併發執行這些線程,因此同時執行多個操做能提高性能。只有多CPU才能獲得提高。 

每一個線程都分配了從0(最低)到31(最高)的優先級。系統決定爲CPU分配那個線程時,以一種輪流方式調度他們。優先級高的會先調度。

直到沒有最高優先級的線程,纔會往下進行調度。這種狀況稱爲 飢餓。較高的優先級佔用了太多的CPU時間,形成較低的線程沒法運行。

應用程序能夠改變相對線程的優先級,向Thread的Priority屬性傳遞ThreadPriority枚舉類型定義的5個值之一。Lowest,BelowNormal,Normal,AboveNormal,Highest。

前臺線程和後臺線程

CLR將線程視爲前臺線程或後臺線程。當一個進程的全部前臺線程中止運行時,會強制終止扔在運行的任何後臺線程。並不會拋出異常。

每一個AppDomain均可以運行一個單獨的應用程序,而每一個應用程序都有本身的前臺線程。若是程序退出,形成他的前臺線程終止,則CLR仍需保持活動並運行,使其餘應用程序退出。

小例子:區分前臺線程和後臺線程。前臺線程中止,則後臺線程會強制中止。

 public sealed class Program
    {
        static void Main(string[] args)
        {
            Thread t = new Thread(Worker);
            t.IsBackground = true;
            t.Start();
            Console.WriteLine("結束");
        }
        private static void Worker()
        {
            Thread.Sleep(1000);
            Console.WriteLine("只有前臺線程才能輸出");
            Console.ReadLine();
        }
    }

在線程的生存期中,任什麼時候候均可以從前臺線程轉變成後臺線程。儘可能避免使用前臺線程,否則會形成進程一直不終止。

線程是很是寶貴的資源,最好的使用方法是CLR線程池。線程池自動爲你管理線程的建立和銷燬。線程池建立的線程將爲各類任務重用,你的應用程序只須要幾個線程就能夠完成所有工做。

建立和銷燬線程是一個昂貴的操做,要消耗大量時間。太多的線程也會浪費內存資源。操做系統必須調度可運行的線程並執行上下文切換,因此太多線程還對性能不理。

爲了改善這種狀況,CLR包含了代碼來管理它本身的線程池,是你的應用程序可以使用的線程集合。線程池由CLR控制的全部AppDomain共享。每一個CLR一個線程池。

CLR初始化時,線程池中是沒有線程的。會在內部維護一個操做請求隊列。執行異步操做時,就調用某個方法,將記錄項追加到線程池的隊列中。若是線程池中沒有線程,就建立一個新線程。

建立新線程會形成必定的性能損失,然而當線程完成任務後,線程不會被銷燬。會返回線程池中,在哪裏進入空閒狀態,等待響應的另外一個請求。因爲不銷燬自身,因此再也不產生額外的性能損失。

若是你的應用程序發出請求的速度超過了線程池線程處理它們的速度,就會建立額外的線程。當線程池空閒時,會自動釋放資源。由於是空閒時間釋放,因此對本身性能損失不大。

將異步操做放入線程池,能夠調用ThreadPool類定義的QueueUserWorkItem方法。他會向線程池添加一個工做項。工做項就是回調方法,必須匹配WaitCallback委託類型。

小例子,演示如何讓一個線程池以異步的方式調用一個方法。

ThreadPool.QueueUserWorkItem(Worker, 5);

private static void Worker(Object state)
        {
            Console.WriteLine("線程池輸出參數:{0}", state);
            Thread.Sleep(1000);
        }

執行上下文

每一個線程都關聯了一個執行上下文的數據結構。執行上下文包括的東西有安全設置、宿主身份、邏輯調用上下文數據。線程執行他的代碼時,一些操做會受到線程執行上下文設置的影響。

理想狀況下,當一個線程使用另外一個線程執行任務時,前者的執行上下文應流向後者。這就確保了後者執行的任何操做都是相同的安全設置和宿主設置。

默認狀況下,CLR自動形成初始線程執行上下文流向任何線程。會自動傳給輔助線程。這會對性能形成必定影響,由於執行上下文包含大量信息,收集、複製到輔助線程,耗費很多時間。

ExecutionContext類,容許你控制線程的執行上下文,如何從一個線程流向另外一個線程。能夠阻止上下文流動,提升性能。

小例子,演示如何阻止執行上下文流動。使用CallContext程序集。

static void Main(string[] args)
        {
            CallContext.LogicalSetData("Name","Jeffrey");
            ThreadPool.QueueUserWorkItem(state => Console.WriteLine("Name={0}",CallContext.LogicalGetData("Name")));
            ExecutionContext.SuppressFlow();
            ThreadPool.QueueUserWorkItem(state => Console.WriteLine("Name={0}", CallContext.LogicalGetData("Name")));
            ExecutionContext.RestoreFlow();
            Console.ReadLine();
        }

協做式取消和超時

FCL提供了標準的取消模式操做。這個模式是協做式的,意味着要取消的操做必須顯示支持取消。對於長時間運行的計算限制操做,支持取消是一件很棒的事情。

取消操做首先須要建立一個 CancellationTokenSource對象。這個對象包含了和管理取消有關的全部狀態。

構造好之後,可從它的Token屬性得到一個或多個CancellationToKen實例。並傳給你的操做,使操做能夠取消。

小例子,進行計數操做,手動中止

public sealed class Program
    {
        static void Main(string[] args)
        {
            CancellationTokenSource cts = new CancellationTokenSource();
            ThreadPool.QueueUserWorkItem(o => Count(cts.Token, 1000));
            Console.ReadLine();
            cts.Cancel();
            Console.ReadLine();
        }

        private static void Count(CancellationToken token, Int32 countTo)
        {
            for (int count = 0; count < countTo; count++)
            {
                if (token.IsCancellationRequested)
                {
                    Console.WriteLine("結束");
                    break;
                }
                Console.WriteLine(count);
                Thread.Sleep(1000);
            }
        }
    }

可調用CancellationTokenSource的Register方法,登記一個或多個在取消時調用的方法。

static void Main(string[] args)
        {
            var cts1 = new CancellationTokenSource();
            cts1.Token.Register(()=>Console.WriteLine("取消時會調用此方法"));
            cts1.CancelAfter(10000);
            Console.ReadLine();
        }

CancelAfter能夠在指定時間後進行取消操做。

線程池的方法有不少技術限制。最大的問題是沒有機制讓你知道操做在何時完成,也沒有機制在操做完成時得到返回值。爲了克服這些限制,引入了 任務 的概念。

使用任務作相同的事情

ThreadPool.QueueUserWorkItem(Worker, 5);
new Task(Worker, 5).Start();
Task.Run(() => Worker(5));

調用Task構造器須要傳一個Action或Action<Object>委託,調用Run時能夠傳遞一個Action或Func<T>委託。無論是構造器仍是Run方法,均可以傳遞取消任務。

還能夠向構造器傳遞一些 TaskCreationOptions 標誌來控制 Task 的執行方式。枚舉中定義的部分是提議,部份內容總會被採納。

等待任務返回結果

class Program
    {
        static void Main(string[] args)
        {
            Task<Int32> t = new Task<Int32>(n => Sum((Int32)n), 1000);
            t.Start();
            t.Wait();
            Console.WriteLine("The Sum is : " + t.Result);
            Console.Read();
        }

        private static Int32 Sum(Int32 n)
        {
            Int32 sum = 0;
            for (; n > 0; n--)
            {
                checked
                {
                    sum += n;
                }
            }
            return sum;
        }
    }

線程調用Wait方法時,系統檢查線程要等待的Task是否開始執行。若是開始會阻塞線程,直到Task結束。

若是Task尚未執行,系統可能使用調用Wait的線程來執行Task。

除了單個等待這個方法之外,Task還提供了兩個靜態方法。

WaitAny 方法容許等待一個Task對象數組,直到數組中任何的Task對象完成。方法返回數組索引,指明完成的是那個Task對象。超時返回-1;

WaitAll 方法容許等待一個Task對象數組,直到全部Task對象都完成,WaitAll 返回true,超時則返回false。

取消任務

使用CancellationTokenSource 取消Task。例子:

class Program
    {
        static void Main(string[] args)
        {
            CancellationTokenSource cts = new CancellationTokenSource();
            Task<int> t = Task.Run(() => Sum(cts.Token, 100000), cts.Token);
            cts.Cancel();
            try
            {
                Console.WriteLine("The Sum is:" + t.Result);
            }
            catch (AggregateException x)
            {
                x.Handle(e => e is OperationCanceledException);
                Console.WriteLine("Sum was Canceled");
            }
            Console.Read();
        }

        private static Int32 Sum(CancellationToken ct, Int32 n)
        {
            Int32 sum = 0;
            for (; n > 0; n--)
            {
                ct.ThrowIfCancellationRequested();
                checked { sum += n; }
            }
            return sum;
        }
    }

ct.ThrowIfCancellationRequested(); 定時檢查操做是否已取消。若是取消了,就會拋出OperationCanceledException。

由於任務有辦法表示完成,任務能返回一個值。因此須要採起一種方式將已完成的任務和出錯的任務區分開。

任務取消的話,首先會拋出AggregateException 這個異常,因此先捕獲這個異常,而後將任何Operation.Exception對象都視爲已處理

建立Task構造器的時候,將CancellationToken傳給構造器,進行二者關聯。

若是在Task開始前取消,會拋出異常。若是已經開始,那麼只有顯示支持取消纔可以在執行期間取消。

Task對象關聯的取消任務,在Task構造委託Action中,沒有辦法拿到。爲此,最簡單的辦法就是使用Lambda表達式,做爲閉包變量傳遞,如例。

任務完成時自動啓動新任務

使用Task的ContinueWith方法,能夠在任務完成時啓動另外一個任務。他不會阻塞任何線程:

static void Main(string[] args)
        {
            Task<int> t = Task.Run(() => Sum(CancellationToken.None, 10000));
            Task cwt = t.ContinueWith(task => Console.WriteLine(t.Result));

            Console.Read();
        }

使用TaskContinuationOptions枚舉值,指定後續任務執行狀態。例如:指定只有在第一個任務拋出未處理異常的時候才執行、只有順利完成的時候才執行。

默認狀況下,若是不指定狀態標誌,則新任務不管如何都會執行。

任務父子關係

任務能夠建立父子關係,父任務建立多個Task對象,除非全部子任務結束運行,不然父任務不認爲已經結束。

static void Main(string[] args)
        {
            Task<Int32[]> parent = new Task<int[]>(()=> {
                var result = new Int32[3];
                new Task(()=>result[0] = Sum(100),TaskCreationOptions.AttachedToParent).Start();
                new Task(() => result[1] = Sum(200), TaskCreationOptions.AttachedToParent).Start();
                new Task(() => result[2] = Sum(300), TaskCreationOptions.AttachedToParent).Start();
                return result;
            });
            parent.Start();
            var cwt = parent.ContinueWith(p=>Array.ForEach(parent.Result,Console.WriteLine));
            Console.Read();
        }

使用AttachedToParent進行關聯操做,將延續任務制定成子任務。

Task.TaskStatus 值,制定了其生存期的狀態。首次構造對象,它的狀態是Created,任務啓動時,它的狀態爲WaitingToRun等等

爲了簡化生存期狀態,Task提供了幾個Boolean屬性,用來判斷狀態。例如:IsCanceled,IsFaulted。

例如判斷一個Task是否成功完成,最簡單的辦法是:if(task.Status == TaskStatus.RunToCompletion){...}

有時須要建立一組共享相同配置的Task對象,爲了不將相同的參數傳給每一個Task的構造器。可建立一個任務工廠來封裝通用的配置。

Tasks 命名空間定義了一個 TaskFactory 類型和 TaskFactory<T> 要向任務工廠傳遞但願任務具備的CancellationToKen取消、TaskScheduler隊列、TaskCreationOptions、TaskContinuationOptions

下面例子演示使用任務工廠

static void Main(string[] args)
        {
            Task parent = new Task(() =>
            {
                var cts = new CancellationTokenSource();
                var tf = new TaskFactory<Int32>(cts.Token, TaskCreationOptions.AttachedToParent, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);

                var childTasks = new[] {
                    tf.StartNew(()=>Sum(cts.Token,1000)),
                    tf.StartNew(()=>Sum(cts.Token,2000)),
                    tf.StartNew(()=>Sum(cts.Token,int.MaxValue))
                };

                //任務拋出異常,取消其他子任務
                for (int task = 0; task < childTasks.Length; task++)
                {
                    childTasks[task].ContinueWith(t => cts.Cancel(), TaskContinuationOptions.OnlyOnFaulted);
                }

                //從爲出錯/未取消的任務中,將返回的最大值用另外一個任務來進行顯示
                tf.ContinueWhenAll(
                        childTasks,
                        completedTasks => completedTasks.Where(t => !t.IsFaulted && !t.IsCanceled).Max(t => t.Result),
                        CancellationToken.None)
                        .ContinueWith(t => Console.WriteLine("max is {0}", t.Result),TaskContinuationOptions.ExecuteSynchronously);
            });

            //子任務完成後,顯示任何未處理的異常
            parent.ContinueWith(p => {
                StringBuilder sb = new StringBuilder("The following :" + Environment.NewLine);
                foreach (var e in p.Exception.Flatten().InnerExceptions)
                {
                    sb.AppendLine(" " + e.GetType().ToString());
                }
                Console.WriteLine(sb.ToString());
            }, TaskContinuationOptions.OnlyOnFaulted);

            parent.Start();
            Console.Read();
        }

建立一個任務工廠,該工廠建立了三個Task對象。他們都共享取消標記。都被視爲其父任務的子任務,建立的全部任務都以同步的方式執行。都使用默認的TaskScheduler

全部子任務都調用TaskFactory的StartNew方法來建立,使用該方法能夠很方便地建立並啓動子任務。

經過一個循環告訴每一個子任務,若是拋出未處理的異常,就取消其餘仍在運行的全部子任務。 

在TaskFactory上調用ContinueWhenAll,建立全部子任務完成後啓動的一個任務,傳遞一個CancellationToken.None使任務不能取消。

處理完全部結果之後,返回最大值。

性能的提高

一些常見的編程情形可經過任務提升性能。爲簡化編程,Tasks.Parallel類封裝了這些情形,它內部使用Task對象。主要針對循環和並行執行進行簡化。

例如,不要像下面這樣處理集合中的全部項:

for(int i=0;i<1000;i++) Dowork(i);  //每次循環調用一次Dowork

使用Parallel類的For方法,採起多線程輔助完成

Parallel.For(0, 50, i => Dowork(i));

 foreach 同理

IEnumerable<int> collection = new List<int> { };
Parallel.ForEach(collection, i => Console.WriteLine(i));

並行執行多個方法

Parallel.Invoke(
                () => { Thread.Sleep(1000); Console.WriteLine(123); },
                () => { Thread.Sleep(1000); Console.WriteLine(123); },
                () => { Thread.Sleep(1000); Console.WriteLine(123); }
                );

Parallel的全部方法都讓調用線程參與處理。可是並非全部的for都要替換成Parallel.For。前提條件是:工做項必須能並行執行,若是須要順序執行,就不要使用這個方法。

另外,要避免會修改任何共享數據的工做項,不然多個線程同時處理可能會損壞數據。雖然能夠添加線程同步鎖,但這樣就只有一個線程訪問數據,沒法帶來並行的好處。

定時操做類

Threading 命名空間定義了要給 Timer類,可用它讓一個線程池線程定時調用一個方法。

也可使用Task的靜態Delay方法。例子

 private static async void Status()
        {
            while (true)
            {
                Console.WriteLine(DateTime.Now);
                await Task.Delay(2000);
            }
        }

Status();

PLINQ 多線程併發的集合遍歷查詢

internal class Program
    {
        private static void Main(string[] args)
        {
            List<int> list = new List<int> { 0, 1, 2, 3, 4, 5, 6 };
            var result = from p in list.AsParallel() select p;
            foreach (var item in result)
            {
                Console.WriteLine(item);
            }
            Console.Read();
        }
    }

使用多線程進行查詢,輸出的結果是雜亂無章的。因此若是不須要順序輸出,則使用PLINQ代替LINQ性能會更佳。

C#的異步函數

它容許使用少許線程執行大量操做。與線程池結合,異步操做容許利用機器中全部的CPU。

一旦方法標記爲async,編譯器就會將方法的代碼轉換成實現了狀態機的一個類型。這就容許線程執行狀態機中的一些代碼並返回,方法不須要一直執行到結束。

使用await操做符會在Task對象上調用 ContinueWith,向它傳遞用於恢復狀態機的方法。

異步函數存在如下限制

1.不能將應用程序的Main方法轉變成異步函數。另外構造器、屬性訪問器、事件訪問器不能轉變成異步

函數。

2.異步函數不能使用任何out 或 ref 參數

3.不能在catch,finally或unsafe快中使用 await操做符

4.不能在await操做符以前得到一個支持線程全部權或遞歸的鎖,並在await操做符以後釋放它。由於await以前的代碼由一個線程執行,以後的代碼則可能由另外一個線程執行。在C#lock語句中使用await,編譯器會報錯。

5.在查詢表達式中,await操做符只能在初始from子句的第一個集合表達式中使用,或在join子句集合表達式中使用。

FCL中許多類型都支持異步函數。規範要求爲方法名附加Async後綴。在FCL中,支持I/O操做的許多類型都提供了xxxAsync方法。

有時異步函數須要執行密集的、計算限制的處理,再發起異步操做。若是經過GUI線程來調用函數,UI就會忽然失去響應,好長時間才能恢復。

另外若是操做以同步方式完成,那麼UI失去顯影的時間還會更長。在這種狀況下,能夠利用Task的靜態Run方法從其餘線程中執行異步函數。

static void Main(string[] args)
        {
            Task.Run(async () => {
                await Status();
            });

            Console.Read();
        }

        private static async Task Status()
        {
            while (true)
            {
                Console.WriteLine(DateTime.Now);
                await Task.Delay(2000);
            }
        }

msdn關於線程同步

在應用程序中使用多線程的好處是每一個線程均可以異步執行,對於Windows應用程序,耗時的任務能夠在後臺執行。而使應用程序窗口和控件保持響應。

然而,線程的異步特性意味着必須協調對資源(文件、網絡鏈接、內存)的訪問。不然,兩個或更多的線程可能在同一時間訪問相同的資源,而每一個線程都不知道其餘線程的操做。結果將產生不可預知的數據破壞。

對於整數數據類型的簡單操做,能夠用Interlocked類的成員來實現線程同步。

對於其餘全部數據類型和非線程安全的資源,只有使用lock 結構才能安全的執行多線程處理。 

lock 語句

能夠用來確保代碼塊完成運行,而不會被其餘線程中斷。這是經過在代碼塊運行期間爲給定對象獲取互斥鎖來實現的。

lock有一個做爲參數的對象,在該參數後面還有一個一次只能由一個線程執行的代碼塊。例如:

internal class Program
    {
        private static object lockThis = new object();

        private static void Main(string[] args)
        {
            lock (lockThis)
            {
                Console.WriteLine("這裏執行一些操做");
            }
            Console.Read();
        }
    }

提供給lock關鍵字的參數必須爲基於引用類型的對象,該對象用來定義鎖的範圍。上面的例子中,鎖的範圍是這個方法,由於函數外不存在任何對對象lockThis的引用。

若是確實存在此類引用,鎖的範圍將擴展到該對象。嚴格地說,提供的對象只是用來惟一地標識多個線程共享的資源,能夠是任何類的實例。

lock關鍵字將語句塊標記爲臨界區,方法是獲取給定對象的互斥鎖,執行語句,而後釋放該鎖。

lock關鍵字可確保當一個線程位於代碼的臨界區時,另外一個線程不會進入該臨界區。若是其餘線程嘗試進入鎖定的代碼,則它將一直等待,直到該對象被釋放。

lock關鍵字在快開始的時候調用Enter,在快結尾的時候調用Exit。

若是一個容器對象被多個線程使用,則能夠將該容器傳遞給lock,而lock後面的同步代碼將訪問該容器。

只要其餘線程在訪問容器前先鎖定該容器,則對該對象的訪問將是安全同步的。應避免鎖定public類型,不然實例將超出代碼的控制範圍。

最佳的作法是定義private對象來鎖定,或 private static 對象變量來保護全部實例所共有的數據。

若是該實例能夠被公開訪問,如 lock(this) 可能會有問題,由於不受控制的代碼也可能會鎖定該對象。這可能致使死鎖,即兩個或更多個線程等待釋放同一個對象。

出於一樣的緣由,鎖定公共數據類型也可能致使問題。鎖定字符串尤爲危險,由於字符串被CLR暫留。這意味着整個程序中任何給定字符串都只有一個實例。

就是這同一個對象表示了全部運行的應用程序域全部線程中的該文本。所以,只要在應用程序進程中的任何位置處具備相同內容的字符串上放置鎖,就將鎖定應用程序中該字符串的全部實例。

最好鎖定不會被暫留的私有或受保護成員。某些類提供專門用於鎖定的成員。

使用lock關鍵字一般比使用 Monitor 類更可取,一方面由於更簡潔,另外一個方面lock確保了即便受保護代碼引起異常,也能夠釋放基礎監視器。 

但使用lock同步應該注意一下五點:

1.同步對象在須要同步的多線程中是可見的同一個對象

2.在非靜態方法中,靜態變量不該做爲同步對象

3.值類型對象不能做爲同步對象

4.避免對字符串做爲同步對象

5.下降同步對象的可見性

看一個稍微正經一點的例子:開10個線程,而且讓他們作一個循環計算,讓每一個循環暫停5毫米用於測試循環中破壞資源。每一個線程都訪問一個共享資源,而且會改變這個資源。

internal class Program
    {
        public static long total = 0;
        private static object lockThis = new object();

        private static void Main(string[] args)
        {
            Stopwatch stop = new Stopwatch();
            stop.Start();
            Program pro = new Program();
            Task parent = new Task
                (() =>
                {
                    for (int i = 0; i <= 10; i++)
                    {
                        new Task(obj => { pro.add(obj); }, (i + 1), TaskCreationOptions.AttachedToParent).Start();
                    }
                });
            parent.Start();
            parent.ContinueWith(p => { stop.Stop(); Console.WriteLine(stop.Elapsed); });
            Console.Read();
        }

        public void add(object text)
        {
            total = 0;
            for (int i = 0; i < 50; i++)
            {
                total++;
                Thread.Sleep(5);
            }
            Console.WriteLine("第{0}個線程:" + total, text);
        }
    }

PS:由於想計算加lock和不加lock的時間,使用線程池的話,不知道什麼時候完成,因此在這裏使用Task異步。看一下運行結果程序

 執行2.49秒。我對一樣的數據,進行一樣的計算,可是獲得不一樣的結果。是由於有其餘線程破壞了 total 變量。致使最後輸出有問題。

那麼咱們使用 lock 鎖,改爲下面的樣子。

public void add(object text)
        {
            lock (lockThis)
            {
                total = 0;
                for (int i = 0; i < 50; i++)
                {
                    total++;
                    Thread.Sleep(5);
                }
                Console.WriteLine("第{0}個線程:" + total, text);
            }
        }

這個時候,咱們把total鎖定,只有一個線程能夠訪問。再看一下結果。

線程同步了,可是加鎖的程序比不加鎖的程序,慢了7倍。因此,程序中是否須要加鎖要慎重的考慮。 

與鎖相比的另外一個等待機制,就是信號同步。

信號同步機制中涉及的類型都繼承自抽象類 WaitHandle。這些類型有 EventWaitHandle(AutoResetEvent,ManualResetEvent)、Semaphore、Mutex

AutoResetEvent:通知正在等待的線程已發生事件。他們都繼承WaitHandle,維護一個由內核產生的布爾對象(阻滯狀態),若是爲false,那麼在它上面等待的線程就阻塞。

能夠調用類型的Set方法將其值設置爲true,解除阻塞。下面用一個簡單的例子,來演示信號同步

internal class Program
    {
        private static void Main(string[] args)
        {
            AutoResetEvent auto = new AutoResetEvent(false);
            Thread tWork = new Thread(() =>
            {
                Console.WriteLine("此時,開始進入等待狀態");
                auto.WaitOne(); //阻塞線程
                Console.WriteLine("我結束等待狀態了");
            });
            tWork.Start();
            Console.Read();
            auto.Set(); //我發送結束阻塞

            Console.Read();
        }
    }

ManualResetEvent 與 AutoResetEvent的區別是:後者在發送信號完畢後(調用Set方法)會自動將本身的阻滯狀態設置爲false。而前者須要進行手動設定。

例如,若是同時兩個線程進行阻塞狀態,AutoResetEvent 在發送信號後,會自動設置爲false。因此只會有一個線程收到,另外一個線程不會受到。

這種時候,使用 ManualResetEvent 就能夠實現。

internal class Program
    {
        private static void Main(string[] args)
        {
            ManualResetEvent auto = new ManualResetEvent(false);
            Thread tWork = new Thread(() =>
            {
                Console.WriteLine("此時,開始進入等待狀態");
                auto.WaitOne(); //阻塞線程
                Console.WriteLine("我結束等待狀態了");
            });
            Thread tWork1 = new Thread(() =>
            {
                Console.WriteLine("1.此時,開始進入等待狀態");
                auto.WaitOne(); //阻塞線程
                Console.WriteLine("1.我結束等待狀態了");
            });
            tWork.Start();
            tWork1.Start();
            Console.Read();
            auto.Set(); //我發送結束阻塞
        }
    }

信號同步機制,也能夠進行模擬心跳操做。

 internal class Program
    {
        private static void Main(string[] args)
        {
            AutoResetEvent auto = new AutoResetEvent(false);
            Thread tWork = new Thread(() =>
            {
                while (true)
                {
                    bool re = auto.WaitOne(3000);
                    if (!re)
                    {
                        Console.WriteLine("沒有收到心跳");
                    }
                }
            });
            tWork.IsBackground = true;
            tWork.Start();
            Console.Read();
            auto.Set(); //我發送結束阻塞
        }
    }

多個線程同時訪問共享數據時,線程同步能防止數據損壞。若是一些數據由兩個線程訪問,但不可能同咱們時接觸到數據,就徹底用不到線程同步。

不須要線程同步是最理想的狀況,由於線程同步存在許多問題:

1.它比較繁瑣,並且很容易寫錯。

2.它們會損害性能,獲取和釋放鎖是須要時間的。

3.它們一次只容許一個線程訪問資源。阻塞一個線程會形成更多的線程被建立。進一步損害性能。

線程同步是一個很很差的事情,應該儘量的避免進行線程同步操做。 

FCL保證全部的靜態方法都是線程安全的。這意味着加入兩個線程同時調用一個靜態方法,不會發生數據被破壞的狀況。

線程安全的方法意味着何在兩個線程試圖同時訪問數據時,數據不會被破壞。例如:Math有一個靜態的Max方法

public static Int32 Max(Int32 vall, Int32 val2)        
{
    return(vall < val2) ?val2 : vall;        
}

這個方法是線程安全的,即便它沒有獲取任何鎖。Int32是值類型,傳給Max的兩個Int32值會複製到方法內部。多個線程能夠同時調用Max方法,互不干擾。

FCL不保證明例方法是線程安全的。假如每一個實例方法都須要獲取和釋放一個鎖,那麼會形成在最終時刻你的應用程序只有一個線程在運行。 

基元用戶模式和內核模式構造基元是指能夠在代碼中使用的最簡單的構造。基元模式的構造有兩種:用戶模式和內核模式。

應當儘可能使用用戶模式構造,它們的速度要明顯快於內核模式的構造。由於是使用特殊的CPU命令來協助線程。這意味着協調是在硬件中發生的。但也意味着操做系統檢測不到線程在用戶模式上阻塞。不會認爲阻塞,並且CPU指令只阻塞線程至關短的時間。

缺點是:只有Windows操做系統內核才能中止一個線程的運行。用戶模式中運行的線程可能被系統搶佔,但線程會以最快的速度再次調度。因此,想要取得資源但暫時取不到的線程會一直自旋。這會浪費大量CPU時間。

內核模式的構造是由Windows操做系統自身提供的。因此,它們要求在應用程序的線程中調用由操做系統內核實現的函數。

他有一個重要的優勢,線程經過內核模式的構造獲取其餘線程擁有的資源時,Windows會阻塞線程以免浪費CPU時間。當資源變得可用時,會恢復線程,容許它訪問資源。

對於在構造上等待的線程,若是擁有這個構造的線程一直不釋放它,前者就可能一直阻塞。

若是用戶模式構造,線程將一直在一個CPU上運行,咱們稱爲 活鎖

若是內核模式構造,線程將一直阻塞,咱們稱爲 死鎖

針對阻塞而言,死鎖 優於 活鎖,由於活鎖會浪費CPU時間和內存,死鎖只浪費內存

理想狀態下,應該結合着兩個鎖的長處。沒有競爭的狀況下,構造不會阻塞。存在競爭的狀況下,但願被操做系統內核阻塞。這種構造稱爲:混合構造。

CLR保證如下數據類型的變量讀寫是原子性的:Boolean,Char,(S)Byte,(U)Int16,(U)Int32,(U)IntPtr,Single以及引用類型。這意味着變量中的全部字段都一次性讀取或寫入。

原子性:全部字段都一次性讀取或寫入。例如:當一個線程改變一個變量時,變量會一次性(原子性)的改變。另外一個線程不可能看處處於中間狀態的值。

兩種基元用戶模式線程同步構造

易變構造:在特定的時間,在包含一個簡單數據類型的變量上執行原子性的讀寫操做。

互鎖構造:在特定的時間,在包含一個簡單數據類型的變量上執行原子性的讀寫操做。

編譯器會將高級構造轉換成低級構造。C#編譯器將你的C#構造轉換成中間語音(IL),而後JIT將IL轉換成本機CPU指令,而後由CPU親自處理這些指令。

在轉換過程當中,C#編譯器、JIT編譯器,甚至CPU自身均可以優化你的代碼。例如以下代碼,就會被優化掉。

internal class Program
    {
        private static void Main(string[] args)
        {
            int value = (1 * 100) - (50 - 2);
            for (int i = 0; i < value; i++)
            {
                Console.WriteLine("這裏永遠不會被執行,因此會被編譯器優化掉");
            }

            Console.Read();
        }
    }

Volatitle類提供了兩個靜態方法,會禁止C#編譯器、JIT編譯器和CPU日常執行的一些優化。

Volatitle.Write 方法強迫location 中的值在調用時寫入。此外,按照編碼順序,以前的加載和存儲操做必須在調用Volatitle.Write 以前發生。

Volatitle.Read 方法強迫location 中的值在調用時讀取。此外,按照編碼順序,以後的加載和存儲操做必須在調用Volatitle.Read 以後發生。

總結:當線程經過共享內存相互通訊時,調用Volatitle.Writel 來寫入最後一個值,調用 Volatitle.Read 來讀取第一個值。

internal sealed class ThreadsSharingData
    {
        private int m_flag = 0;
        private int m_value = 0;

        public void Thread1()
        {
            m_value = 5;
            Volatile.Write(ref m_flag, 1);
        }

        public void Thread2()
        {
            if (Volatile.Read(ref m_flag) == 1)
            {
                Console.WriteLine(m_value);
            }
        }
    }

例如此例,Writel 寫入最後一個值,Read 讀取 第一個值。分析一下這段代碼。

Volatitle.Write 調用確保在它以前的全部寫入。調用以前的寫入可能被優化成以任意順序執行。

Volatitle.Read  調用確保在它以後的全部讀取。調用以後的讀取可能被優化成以任何順序執行。

volatile 關鍵字,能夠應用任何類型的靜態或實例字段。JIT編譯器確保對易變字段的全部訪問都是以一邊讀取或寫入的方法執行。

Interlocked 每一個方法都執行一次原子讀取以及寫入操做。調用某個Interlocked方法以前的任何變量都卸載這個Interlocked方法調用以前執行,讀取則在調用以後讀取。

internal sealed class ThreadsSharingData
    {
        private int m_flag = 0;
        private int m_value = 0;

        public void Thread1()
        {
            m_value = 5;
            Interlocked.Add(ref m_flag, 1);
        }

        public void Thread2()
        {
            if (Interlocked.Decrement(ref m_flag) == 0)
            {
                Console.WriteLine(m_value);
            }
        }
    }

自旋鎖 能夠在某種狀況下,阻止全部線程,只容許其中一個對字段進行操做。例如

internal struct SimpleSpinLock
    {
        private Int32 m_ResourceInUse;

        public void Enter()
        {
            while (true)
            {
                if (Interlocked.Exchange(ref m_ResourceInUse, 1) == 0)
                    return;
                Thread.Sleep(10000);
            }
        }

        public void Leave()
        {
            Volatile.Write(ref m_ResourceInUse, 0);
        }
    }


m_sl.Enter();
            //不會同時訪問這裏
            Console.WriteLine("這裏不會同時被訪問到");
            m_sl.Leave();

使用while來進行一個自旋操做。而且使用m_resourceInUse進行標註,使用過變成1,未使用變成0。這樣能夠確保只有一個線程會訪問到一個資源。

可是這種自旋鎖會浪費CPU時間,阻止CPU作其餘更有用的工做。

相關文章
相關標籤/搜索