2013年7月26日 星期五

關於 GC 以及 IDisposable 的兩三事


.Net Framework 對於 Dispose() 有以下的不成文規定:
1. 一旦 Dispose() 過,物件的 Methods/Properties/Indexers 被呼叫時應該丟 ObjectDisposedException
2. 允許重複呼叫 Dispose() 
3. Dispose() 上層物件應當會同時 Dispose() 掉他連結的下層物件 (例如 DeflateStream 以及 MemoryStream 的例子)

撰寫解構子 (Finalizer) 時有下列注意事項:
1. 請確保你的解構子可以在最短的時間內完成
2. 絕對不要在解構子裡 block 住
3. 不要在解構子裡參用其他含有解構子的物件
4. 絕對不要在解構子產生未處理的例外
5. 注意就算建構時發生例外,解構子仍然會被呼叫
6. 解構子只會被呼叫一次,除非透過 GC.ReRegisterForFinalize 來重新註冊
7. 注意解構子沒有固定的執行順序
8. 注意執行解構子的執行緒不一定只有一條

一些小知識:
Gen0 的垃圾回收通常可以在 1ms 內結束,但 Gen2 GC 可能會花超過 100ms,幸好大部分的 GC 都是 Gen0 GC。

CLR 在做 Gen0 以及 Gen1 GC 時,會 hold 住系統內所有執行緒。但 workstation version CLR(一般都是此組態) 在做 Gen2 GC 時,不會 hold 住其他執行緒。

透過 GC.Collect 可以強迫系統 GC,通常都這樣寫:
GC.Collect(); // 發動第一次 GC,不含解構子的垃圾會馬上被清除
GC.WaitForPendingFinalizers();  // 等待所有的解構子被執行
GC.Collect(); // 再發動一次 GC,含有解構子的物件會在此時被清除

CLR 具有自我調節垃圾回收演算法的機能,而程式主動 GC 會打亂這個機能。因此除非有很好的理由(例如做效能測試),否則請不要主動呼叫 GC。

出處:C# 5.0 In a Nutshell Ch12

例外處理的效能


C# 的例外處理,若無 catch 到例外,則有無 try catch 幾乎不影響效能。若有 catch 到例外,則對效能會有極大的影響。

using System;
using System.Diagnostics;

namespace ConsoleApplication1 {
    class Program {
        static void Foo() {
            try { throw new InvalidOperationException(); } catch { }
        }
        static void Main(string[] args) {
            const Int32 REPEAT = 1000000;
            Stopwatch stopwatch = Stopwatch.StartNew();
            for(Int32 i=0; i < REPEAT; i++) { Foo(); }
            stopwatch.Stop();
            Console.WriteLine(((Double)REPEAT)/(((Double)(stopwatch.ElapsedMilliseconds)) / 1000.0));
            Console.ReadKey();
        }
    }
}

上面的 C# 程式,編譯成 Release 版,在我的機器上輸出為 72259。同樣的程式用 C++ 寫,其實也沒有快多少:

void Foo() {
    try { throw 0; } catch(...) {}
}
int main(int argc, char* argv[]) {
    const int REPEAT = 1000000;
    DWORD from = ::timeGetTime();
    for(int i=0; i < REPEAT; i++) { Foo(); }
    DWORD to = ::timeGetTime();
    std::cout << double(REPEAT)/(double(to - from)/1000.0) << std::endl;
    std::cin.get();
    return 0;
}

上面的 C++ 程式的輸出是 125802。

結論:try catch 應該用來捕捉不常發生的錯誤。發生的機率很頻繁,或稱不上是錯誤的狀態,應該考慮用其他方法回報。

Atomic Operations


原子運算指的是不會被切割的運算 (可以想像成在進行原子運算時,系統偷偷幫我們上了鎖)。舉個例子,下面這支程式裡,count++ 並不是原子運算,因此最後顯示出來的結果不一定會是 2000000:

using System;
using System.Threading;

namespace ConsoleApplication2 {
    class Program {
        static Int32 count = 0;
        static void Main(string[] args) {
            const Int32 REPEAT = 1000000;
            Thread th1 = new Thread(() => {    
                for(Int32 i = 0; i < REPEAT; i++) count++;
            });
            Thread th2 = new Thread(() => {
                for(Int32 i = 0; i < REPEAT; i++) count++;
            });
            th1.Start();    th2.Start();
            th1.Join();     th2.Join();
            Console.WriteLine(count);
            Console.ReadKey();
        }
    }
}

下面這支程式裡,Interlocked.Increment(ref count) 是原子運算,因此最後顯示出來的結果一定會是 2000000:

using System;
using System.Threading;

namespace ConsoleApplication2 {
    class Program {
        static Int32 count = 0;
        static void Main(string[] args) {
            const Int32 REPEAT = 1000000;
            Thread th1 = new Thread(() => {
                for(Int32 i = 0; i < REPEAT; i++) Interlocked.Increment(ref count);

            });
            Thread th2 = new Thread(() => {
                for(Int32 i = 0; i < REPEAT; i++) Interlocked.Increment(ref count);
            });
            th1.Start();    th2.Start();
            th1.Join();     th2.Join();
            Console.WriteLine(count);
            Console.ReadKey();
        }
    }
}

C# 規格書(C# Specification)裡,明白的規定了下面事項:

12.5 Atomicity of variable references 

Reads and writes of the following data types shall be atomic: bool, char, byte, sbyte, short, ushort, uint, int, float, and reference types. In addition, reads and writes of enum types with an underlying type in the previous list shall also be atomic. Reads and writes of other types, including long, ulong, double, and decimal, as well as user-defined types, need not be atomic. Aside from the library functions designed for that purpose, there is no guarantee of atomic read-modify-write, such as in the case of increment or decrement.

翻譯:
以下的型別變數的【讀取】與【寫入】保證是原子運算: bool, char, byte, sbyte, short, ushort, uint, int, float, 以及 reference types。另外,以上述型別為基底的 enum types 其【讀取】與【寫入】也保證是原子運算。其他型別的【讀取】與【寫入】則不保證為原子運算。【讀-改-寫】 類型的運算,例如 ++、--、+=、-= 之類的,不保證為原子運算。

Int32 i = 3; // 原子運算
Int64 j = 3; // 不保證為原子運算
Int32 k += i; // 不保證為原子運算
i++; // 不保證為原子運算
Object o1 = null; // 原子運算
Object o2 = o1; // 原子運算

以下是一個非原子運算所導致的競走問題。把 i64 的型別改為 UInt32 就會好。

using System;
using System.Threading;

namespace ConsoleApplication2 {
    class Program {
        static UInt64 i64 = 0;
        static void Main(string[] args) {
            Thread th1 = new Thread(() => {                
                while(true) i64 = 0;
            });
            Thread th2 = new Thread(() => {
                while(true)
                    i64 = 0xFFFFFFFFFFFFFFFFul;
                ;
            });
            th1.Start();    th2.Start();
            UInt32 good = 0, bad = 0;
            while(true) {
                UInt64 tmp = i64;
                if(tmp != 0 && tmp != 0xFFFFFFFFFFFFFFFFul) {
                    bad++;
                    Console.WriteLine("Opps good(" + good.ToString() + 
                        ") bad(" + (bad).ToString() + ")");
                } else {
                    good++;
                }
            }
        }
    }
}

關於 Stream 的兩三事


.NET 的 Stream 架構被切割成三份 (參照附圖)。

# Close 或 Dispose 一個 Decorator 會順便 Close 或 Dispose 掉附屬的 Backing Store Stream。

# Stream 並非 Thread-Safe,但所有的 Stream 子代都可以透過 Synchronized 這個 Method 來取得有上鎖的對應版本。(例如 FileStream.Synchronized)

# 關閉 Adapter 以及他附屬的 Stream 有四種作法:
1. Close the adapter only.
2. Close the adapter, and then close the stream.
3. (For writers) Flush the adapter, and then close the stream.
4. (For readers) Close just the stream.

若 Adapter 沒有呼叫過 Close 或 Dispose,那麼該 Adapter 被垃圾回收時,CLR 並不會把他附屬的 Stream Close 或 Dispose 掉 (這其實不是 CLR 的功勞,而是所有 Adapter 的實作都保證這件事)。這讓我們可以丟棄 adapter 而繼續使用他底下的 stream (注意! Decorator 就不是這樣了)。

.Net Framework 4.5 額外替 Adapter 的建構子提供了一個參數,用來標明 Close 或 Dispose 要不要順便 Close 或 Dispose 附屬的 Stream。如下程式所示:

using (var fs = new FileStream ("test.txt", FileMode.Create)) {
  using (var writer = new StreamWriter (fs, new UTF8Encoding (false, true),
                                       0x400, true))
    writer.WriteLine ("Hello");

  fs.Position = 0;
  Console.WriteLine (fs.ReadByte());
  Console.WriteLine (fs.Length);
}



出處:C# 5.0 In a Nutshell - 5Ed, Ch15

關於 call method 的兩三事


CLR 提供兩種不同的 IL 指令來呼叫 method
--------------------------------------------------------------------------
[call]
call 可以用來呼叫所有型態的 method.
透過 call 來呼叫 static method 時,
必須標明 class, 例如:
call void ConsoleApplication1.Sample::StaticMethod()

透過 call 來呼叫 instance 或 virtual method 時,
必須提供一個變數, 用來參考該物件, 例如:

.local init(
    [0] class ConsoleApplication1.Base base2,
    [1] int32 num
)
...
ldloc.0
call instance void ConsoleApplication1.Base::InstanceMethod()

CLR 會根據變數所宣告的型別 (而非被參考的物件的型別),
去找找看該型別有沒有定義該 method,
若無, 則會到他的父代去找, 直到找到為止. (或找不到就丟例外)

另外, call 不會去檢查該變數的值是否為 null.

--------------------------------------------------------------------------
[callvirt]
callvirt 可以用來呼叫 instance 或 virtual method,
但不能用來呼叫 static method.

透過 callvirt 來呼叫 instance 或 virtual method 時,
也必須提供一個變數, 用來參考該物件, 例如:

.local init(
    [0] class ConsoleApplication1.Base base2,
    [1] int32 num
)
...
ldloc.0
callvirt instance void ConsoleApplication1.Base::InstanceMethod()

callvirt 會先檢查該變數的值是不是 null (是的話直接丟例外),
如果被 call 的是 instance method,
則 callvirt 接下來的行為和 call 一模一樣.
但若被 call 的是 virtual method,
則會以被參考物件的實際型別為起點, 往父代找符合的 method.
--------------------------------------------------------------------------

參考下面的 code

using System;
using System.Text;

namespace ConsoleApplication1 {
    class Base {
        public void InstanceMethod() {
            Console.WriteLine( "Base.InstanceMethod()" );
        }
        public virtual void VirtualMethod() {
            Console.WriteLine( "Base.VirtualMethod()" );
        }
        public static void StaticMethod() {
            Console.WriteLine( "Base.StaticMethod()" );
        }
    };
    class Drived : Base {
        public new void InstanceMethod() {
            Console.WriteLine( "Drived.InstanceMethod()" );
        }
        public override void VirtualMethod() {
            Console.WriteLine( "Drived.VirtualMethod()" );
        }
        public new static void StaticMethod() {
            Console.WriteLine( "Drived.StaticMethod()" );
        }
    };
    class Program {
        static void Main( string[] args) {
            Base obj = new Drived();
            obj.InstanceMethod();
            obj.VirtualMethod();
            Drived.StaticMethod();
            Int32 i = 5;
            i.ToString();
        }
    }
}

以下是 Main 的 IL
.method private hidebysig static void Main(string[] args) cil managed
{
    .entrypoint
    .maxstack 1
    .locals init (
        [0] class ConsoleApplication1.Base base2,
        [1] int32 num)
    L_0000: newobj instance void ConsoleApplication1.Drived::.ctor()
    L_0005: stloc.0
    L_0006: ldloc.0
    L_0007: callvirt instance void ConsoleApplication1.Base::InstanceMethod()
    L_000c: ldloc.0
    L_000d: callvirt instance void ConsoleApplication1.Base::VirtualMethod()
    L_0012: call void ConsoleApplication1.Drived::StaticMethod()
    L_0017: ldc.i4.5
    L_0018: stloc.1
    L_0019: ldloca.s num
    L_001b: call instance string [mscorlib]System.Int32::ToString()
    L_0020: pop
    L_0021: ret
}

注意幾個令人意外的地方:

首先, C# 居然用 callvirt 來呼叫 InstanceMethod!!!
這是因為 C# Compiler 認為檢查變數是否為 null 是很重要的.

其次, Int32.ToString()是一個 virtual method,
他 override 的 Object.ToString(),
但 C# Compiler 卻使用 call 來呼叫他!!!
這是因為 Int32 並不能被繼承,
而且變數本身的型別就是 Int32,
因此使用 call 不會造成問題, 這算是一種最佳化的方法.

--------------------------------------------------------------------------
另外一種情況會使得 C# Compiler 使用 call 來呼叫 virtual method,
參考以下程式碼:

class Dummy {
    public override string ToString() {
        return base.ToString();
    }
}

他的 IL 長這樣:

.class auto ansi nested private beforefieldinit Dummy
    extends [mscorlib]System.Object
{
    .method public hidebysig specialname rtspecialname instance void .ctor() cil managed
    {
        .maxstack 8
        L_0000: ldarg.0
        L_0001: call instance void [mscorlib]System.Object::.ctor()
        L_0006: ret
    }

    .method public hidebysig virtual instance string ToString() cil managed
    {
        .maxstack 8
        L_0000: ldarg.0
        L_0001: call instance string [mscorlib]System.Object::ToString()
        L_0006: ret
    }

}

注意 C# Compiler 使用 call 來呼叫 base.ToString().
理由是如果用 callvirt 的話會造成無窮遞迴.


出處: CLR via C# 4Ed Chapter 6

C# 中定義 Event 的標準流程


1. 實作一個  System.EventArgs 的子代, 強烈建議設計成 immutable. C# 建議名字最好叫做 XxxEventArgs, 例如:

public class NewMailEventArgs : EventArgs { ...; }

2. 於發送端 class 定義一個 event member 如下 (不一定要 public), 根據命名規範, 應該要大寫開頭

public class MailManager {
    public event EventHandler<NewMailEventArgs> NewMail;
}

3. 於發送端 class 定義一個 protected virtual method 如下:

public class MailManager {
    protected virtual void OnNewMail(NewMailEventArgs e) {
        EventHandler<NewMailEventArgs> temp = this.NewMail;
        if(temp != null) temp(this, e);
    }
    ...
}

4. 當事件發生時, 呼叫上述的 protected virtual method

-------------------------------------------------------------------------
重點:
步驟 3 之所以要切成兩半, 是為了防止多緒時的 race condition. 因為 delegate 是 immutable, 故只需如步驟 3 所做就可以達成防止 race condition 的需求.

不過步驟 3 有一個問題: 若 compiler 夠聰明, 它可能會將步驟 3 優化如下:
    protected virtual void OnNewMail(NewMailEventArgs e) {
        if(this.newMail != null) this.NewMail(this, e);
    }

CLR 為了保證回溯相容性, 故不會做這種優化. 但為了防止未來版本的 CLR 做此優化, 寫 .NET 4.0 以上的 C# 程式時可以把步驟 3 改成這樣:

public class MailManager {
    protected virtual void OnNewMail(NewMailEventArgs e) {
        EventHandler<NewMailEventArgs> temp = Volatile.Read(ref this.NewMail);
        if(temp != null) temp(this, e);
    }
    ...
}

-------------------------------------------------------------------------
步驟三其實是可以被公式化的, 以下是透過 class extension 來對步驟 3 做公式化的方法:

public static class EventArgExtensions {
    public static void Raise<T>(this T e, Object sender, ref EventHandler<T> eventDelegate) {
        EventHandler<T> temp = Volatile.Read(ref eventDelegate);
        if(temp != null) temp(sender, e);
    }
}

於是原來的步驟三可以寫成這樣:
protected virtual void OnNewMail(NewMailEventArgs e) {
    e.Raise(this, ref this.NewMail);
}

-------------------------------------------------------------------------
EventHandler 其實是一個 delegate, 宣告如下:
public delegate void EventHandler<T>(Object sender, T e);

-------------------------------------------------------------------------
C# Compiler 其實會把下面這宣告

public class MailManager {
    public event EventHandler<NewMailEventArgs> NewMail;
}

偷偷的改寫為這樣:

public class MailManager {
    private EventHandler<NewMailEventArgs> NewMail = null;
    public void add_NewMail(EventHandler<NewMailEventArgs> value) {
        ...; // 將 value 以 thread-safe 的方式加入 this.NewMail
    }
    public voie remove_NewMail(EventHandler<NewMailEventArgs> value){
        ...; // 將 value 以 thread-safe 的方式自 this.NewMail 中移除
    }
}

注意!!! add_XXX 以及 remove_XXX 的宣告修飾詞會與當初 event 的修飾詞一樣. 如果當初的 event 是 virtual 則 compiler 產生出來的這兩個 methods 就會是 virtual. 其他如 protected, private static 亦然.

-------------------------------------------------------------------------
以下是一個事件接收端的案例:
public class Fax {
    public Fax(MailManager mm)  {
        mm.NewMail += this.FaxMsg;
    }
    public void Unregister(MailManager mm) {
        mm.NewMail -= this.FaxMsg;
    }
    private void FoxMsg(Object sender, NewMailEventArgs e) {
        ...; // 處理之
    }
}

上面的 mm.NewMail += this.FaxMsg; 其實會被 compiler 改為:
mm.add_NewMail(new EventHandler<NewMailEventArgs>(this.FaxMsg));

mm.NewMail -= this.FaxMsg; 亦會被 compiler 改為:
mm.remove_NewMail(new EventHandler<NewMailEventArgs>(FaxMsg));



出處: CLR via C# 4Ed Chapter 11