一個月以前我寫了一篇討論字符串的駐留(string interning)的文章,我今天將會以字符串的駐留爲基礎,進一步來討論.NET中的string。string interning的基本前提是string的恆定性(immutability),即string一旦被創建將不會改變。我們就先來談談string的恆定性。
一、 string是恆定的(immutable)
和其他類型比較,string最爲顯著的一個特點就是它具有恆定不變性:我們一旦創建了一個string,在managed heap 上爲他分配了一塊連續的內存空間,我們將不能以任何方式對這個string進行修改使之變長、變短、改變格式。所有對這個string進行各項操作(比如調用ToUpper獲得大寫格式的string)而返回的string,實際上另一個重新創建的string,其本身並不會產生任何變化。
String的恆定性具有很多的好處,它首先保證了對於一個既定string的任意操作不會造成對其的改變,同時還意味着我們不用考慮操作string時候出現的線程同步的問題。在string恆定的這些好處之中,我覺得最大的好處是:它成就了字符串的駐留。
CLR通過一個內部的interning table保證了CLR只維護具有不同字符序列的string,任何具有相同字符序列的string所引用的均爲同一個string對象,同一段爲該string配分的內存快。字符串的駐留極大地較低了程序執行對內存的佔用。
對於string的恆定性和字符串的駐留,還有一點需要特別指出的是:string的恆定性不單單是針對某一個單獨的AppDomain,而是針對一個進程的。
二、 String可以跨AppDomain共享的(cross-appDomain)
我們知道,在一個託管的環境下,Appdomain是託管程序運行的一個基本單元。AppDomain爲託管程序提供了良好的隔離機制,保證在同一個進程中的不同的Appdomain不可以共享相同的內存空間。在一個Appdomain創建的對象不能被另一個Appdomain直接使用,對象在AppDomain之間傳遞需要有一個Marshaling的過程:對象需要通過by reference或者by value的方式從一個Appdomain傳遞到另一個Appdomain。具體內容可以參照我的另一篇文章:用Coding證明Appdomain的隔離性。
但是這裏有一個特例,那就是string。Appdomain的隔離機制是爲了防止一個Application的對內存空間的操作對另一個Application 內存空間的破壞。通過前面的介紹,我們已經知道了string是恆定不變的、是隻讀的。所以它根本不需要Appdomain的隔離機制。所以讓一個恆定的、只讀的string被同處於一個進程的各個Application共享是沒有任何問題的。
String的這種跨AppDomain的恆定性成就了基於進程的字符串駐留:一個進程中各個Application使用的具有相同字符序列的string都是對同一段內存的引用。我們將在下面通過一個Sample來證明這一點。
三、 證明string垮AppDomain的恆定性
在寫這篇文章的時候,我對如何證明string跨AppDomain的interning,想了好幾天,直到我偶然地想到了爲實現線程同步的lock機制。
我們知道在一個多線程的環境下,爲了避免併發操作導致的數據的不一致性,我們需要對一個對象加鎖來阻止該對象被另一個線程 操作。相反地,爲了證明兩個對象是否引用的同一個對象,我們只需要在兩個線程中分別對他們加鎖,如果程序執行的效果和對同一個對象加鎖的情況完全一樣的話,那麼就可以證明這兩個被加鎖的對象是同一個對象。基於這樣的原理我們來看看我們的Sample:
using System.Collections.Generic;
using System.Text;
using System.Threading;
namespace Artech.ImmutableString
{
class Program
{
static void Main(string[] args)
{
AppDomain appDomain1 = AppDomain.CreateDomain("Artech.AppDomain1");
AppDomain appDomain2 = AppDomain.CreateDomain("Artech.AppDomain2");
MarshalByRefType marshalByRefObj1 = appDomain1.CreateInstanceAndUnwrap("Artech.ImmutableString", "Artech.ImmutableString.MarshalByRefType") as MarshalByRefType;
MarshalByRefType marshalByRefObj2 = appDomain2.CreateInstanceAndUnwrap("Artech.ImmutableString", "Artech.ImmutableString.MarshalByRefType") as MarshalByRefType;
marshalByRefObj1.StringLockHelper = "Hello World";
marshalByRefObj2.StringLockHelper = "Hello World";
Thread thread1 = new Thread(new ParameterizedThreadStart(Execute));
Thread thread2 = new Thread(new ParameterizedThreadStart(Execute));
thread1.Start(marshalByRefObj1);
thread2.Start(marshalByRefObj2);
Console.Read();
}
static void Execute(object obj)
{
MarshalByRefType marshalByRefObj = obj as MarshalByRefType;
marshalByRefObj.ExecuteWithStringLocked();
}
}
class MarshalByRefType : MarshalByRefObject
{
Private Fields
Public Properties
Public Methods
}
}
我們來簡單地分析一下上面的coding.
我們創建了一個繼承自MarshalByRefObject,因爲我需要讓它具有跨AppDomain傳遞的能力。在這個Class中定義了兩個爲實現線程同步的helper字段,一個是string類型的_stringLockHelper和object類型的_objectLockHelper,併爲他們定義了相應的Property。此外定義了兩個方法:ExecuteWithStringLocked和ExecuteWithStringLocked,他們的操作類似:在先對_stringLockHelper和_objectLockHelper加鎖的前提下,輸出出操作執行的AppDomain和確切時間。我們通過調用Thread.Sleep模擬10s的時間延遲。
在Main方法中,首先創建了兩個AppDomain,名稱分別爲Artech.AppDomain1和Artech.AppDomain2。隨後在這兩個AppDomain中創建兩個MarshalByRefType對象,併爲它們的StringLockHelper屬性賦上相同的值:Hello World。最後,我們創建了兩個新的線程,並在它們中分別調用在兩個不同AppDomain 中創建的MarshalByRefType對象的ExecuteWithStringLocked方法。我們來看看運行後的輸出結果:
從上面的輸出結果中可以看出,兩個分別在不同線程中執行操作對應的AppDomain的name分別爲Artech.AppDomain1和Artech.AppDomain2。執行的時間(確切地說是操作成功地對MarshalByRefType對象的_stringLockHelper字段進行加鎖的時間)相隔10s,也就是我們在程序中定義的時間延遲。
爲什麼會出現這樣的結果呢?我們只是對兩個處於不同AppDomain的不同的MarshalByRefType對象的stringLockHelper字段進行加鎖。由於我們是同時開始他們對應的線程,照理說它們之間不會有什麼關聯,顯示出來的時間應該是相同的。唯一的解釋就是:雖然這兩個在不同的AppDomain中創建的對象是兩個完全不同的對象,由於他們的stringLockHelper字段具有相同的字符序列,它們引用的是同一個string。這就證明了我們提出的跨AppDomain進行string interning的結論。
爲了進一步印證我們的結論,我們是使兩個MarshalByRefObject對象的stringLockHelper字段具有不同的值,看看結果又如何。於是我們把其中一個對象的stringLockHelper字段改爲”Hello World!”(多加了一個!) 。
marshalByRefObj2.StringLockHelper = "Hello World!";
看看現在的輸出結果,現在的時間是一樣了。
上面我們做的是對string類型字段加鎖的試驗。那麼我們對其他類型的對象進行加鎖,又會出現怎麼的情況呢?我們現在就來做這樣試驗:在各自的線程中調用兩個對象的ExecuteWithObjectLocked方法。我們修改Execute方法和Main()。
{
MarshalByRefType marshalByRefObj = obj as MarshalByRefType;
marshalByRefObj. ExecuteWithObjectLocked ();
}
{
AppDomain appDomain1 = AppDomain.CreateDomain("Artech.AppDomain1");
AppDomain appDomain2 = AppDomain.CreateDomain("Artech.AppDomain2");
MarshalByRefType marshalByRefObj1 = appDomain1.CreateInstanceAndUnwrap("Artech.ImmutableString", "Artech.ImmutableString.MarshalByRefType") as MarshalByRefType;
MarshalByRefType marshalByRefObj2 = appDomain2.CreateInstanceAndUnwrap("Artech.ImmutableString", "Artech.ImmutableString.MarshalByRefType") as MarshalByRefType;
object obj = new object();
marshalByRefObj1.ObjectLockHelper = obj;
marshalByRefObj2.ObjectLockHelper = obj;
Thread thread1 = new Thread(new ParameterizedThreadStart(Execute));
Thread thread2 = new Thread(new ParameterizedThreadStart(Execute));
thread1.Start(marshalByRefObj1);
thread2.Start(marshalByRefObj2);
Console.Read();
}
我們先來看看運行後的輸出結果:
我們發現兩個時間是一樣的,那麼就是說兩個對象的ObjectLockHelper引用的不是同一個對象。雖然上面的程序很簡單,我覺得裏面涉及的規程卻很值得一說。我們來分析下面3段代碼。
marshalByRefObj1.ObjectLockHelper = obj;
marshalByRefObj2.ObjectLockHelper = obj;
簡單看起來,兩個MarshalByRefObject對象的ObjectLockHelper都是引用的同一個對象obj。但是背後的情況沒有那麼簡單。代碼第一行創建了一個新的對象obj,這個對象是在當前AppDomain 中創建的。二對於當前的AppDomain來說,marshalByRefObj1和marshalByRefObj2僅僅是一個Transparent proxy而已,它們包含一個在Artech.AppDomain1和Artech.AppDomain2中創立的MarshalByRefObject對象的引用。我們爲它的ObjectLockHelper複製,對於Transparent proxy對象的賦值調用會傳到真正對象所在的AppDomain,由於obj是當前AppDomain的對象,它不能直接賦給另一個AppDomain的對象。所以它必須經歷一個Marshaling的過程才能被傳遞到另外一個AppDomain。實際上當複製操作完成之後,真正的ObjectLockHelper屬性對應的對象是根據原數據重建的對象,和在當前AppDomain中的對象已經沒有任何的關係。所以兩個MarshalByRefObject對象的ObjectLockHelper屬性引用的並不是同一個對象,所以對它進行加鎖對彼此不要產生任何影響。
四、 從Garbage Collection的角度來看string
我們知道在一個託管的環境下,一個對象的生命週期被GC管理和控制。一個對象只有在他不被引用的時候,GC纔會對他進行垃圾回收。而對於一個string來說,它始終被interning table引用,而這個interning table是針對一個Process的,是被該Process所有AppDomain共享的,所以一個string的生命週期相對比較長,只有所有的AppDomain都不具有對該string的引用時,他纔有可能被垃圾回收。
五、 從多線程的角度來看string
一方面由於string的恆定性,我們不用考慮多線程的併發操作產生的線程同步問題。另一方面由於字符串的駐留,我們在對一個string對象進行加鎖操作的時候,極有可能拖慢這個Application的performance,就像我們的Sample中演示的那樣。而且很有可能影響到處於同一進程的其他Application,以致造成死鎖。所以我們在使用鎖的時候,除非萬不得已,切忌對一個string進行加鎖。
六、 如何高效地使用string
下面簡單介紹一些高效地使用string的一些小的建議:
1. 儘量使用字符串(literal string)相加來代替字符串變量和字符創相加,因爲這樣可以使用現有的string操作指令進行操作和利用字符串駐留。
比如:
優於
s = s + "def";
2. 在需要的時候使用StringBuilder對string作頻繁的操作:
由於string的恆定性,在我們對一個string進行某些操作的時候,比如調用ToUpper()或者ToLower()把某個string每個字符轉化成大寫或者小寫;調用SubString()取子串;會創建一個新的string,有時候會創建一些新的臨時string。這樣的操作會增加內存的壓力。所有在對string作頻繁操作的情況下,我們會考慮使用StringBuilder來高效地操作string。StringBuilder之所以能對string操作帶來更好的performance,是因爲在它的內部維護一個字符數組,而不是一個string來避免string操作帶來的新的string的創建。
StringBuilder是一個很好的字符累加器,我們應該充分地利用這一個功能:
sb.Append(str1 + str2);
最好寫成
sb.Append(str1);
sb.Append(str2);
避免創建一個新的臨時string來保存str1 + str2。
再比如下面的Code
sb.Append(WorkOnString1());
sb.Append(WorkOnString2());
sb.Append(WorkOnString3());
最好寫好吧WorkOnString1,WorkOnString2,WorkOnString3定義成:
WorkOnString2(StringBuilder sb)
WorkOnString3(StringBuilder sb)
3. 高效地進行string的比較操作
我們知道,對象之間的比較有比較Value和比較Reference之說。一般地對Reference進行比較的速度最快。對於string,在字符串駐留的前提下,我們可以把對Value的比較用Reference的比較來代替從而會的Performance的提升。
此外,對於忽略大小寫的比較,我們最好使用string的static方法Compare(string strA, string strB, bool ignoreCase)。也就是說:
最好寫成
原文:http://www.cnblogs.com/artech/archive/2007/05/06/737130.html