1. 程式人生 > >C#中字串 "駐留"與Lock(轉載)

C#中字串 "駐留"與Lock(轉載)

class TestWorker
 2 {        
 3     public void DoMultiThreadedWork(object someParameter)
 4     {
 5         lock (lockObject)
 6         {
 7             // 
 lots of work 
 8         }
 9     }
10 
11     private string lockObject = "lockit";
12 

這段程式碼很簡單,大家看看這段程式碼有什麼問題呢?

 

 

開始這麼一看似乎沒什麼大的問題。可是仔細分析一下程式碼你就可以知道其中還是有一些很嚴重的問題的。假如你對Lock機制有所瞭解並且對string型別有過研究的話你就會發現出問題:

lock 關鍵字的引數必須為基於引用型別的物件,該物件用來定義鎖的範圍。在上例中,鎖的範圍限定為此函式,因為函式外不存在任何對該物件的引用。嚴格地說,提供給 lock 的物件只是用來唯一地標識由多個執行緒共享的資源,所以它可以是任意類例項。然而,實際上,此物件通常表示需要進行執行緒同步的資源。例如,如果一個容器物件將被多個執行緒使用,則可以將該容器傳遞給 lock,而 lock 後面的同步程式碼塊將訪問該容器。只要其他執行緒在訪問該容器前先鎖定該容器,則對該物件的訪問將是安全同步的。通常,最好避免鎖定 public 型別或鎖定不受應用程式控制的物件例項。因為不受控制的程式碼也可能會鎖定該物件。這可能導致死鎖,即兩個或更多個執行緒等待釋放同一物件。關於Lock被編譯器編譯過後的程式碼請參考我的另一篇文章:

Inside C#

StringCLR中有兩個重要的屬性:不變性和字串駐留。這意味著整個程式中任何給定字串都只有一個例項,就是這同一個物件表示了所有執行的應用程式域的所有執行緒中的該文字。因此,只要在應用程式程序中的任何位置處具有相同內容的字串上放置了鎖,就將鎖定應用程式中該字串的所有例項。因此,最好鎖定不會被暫留的私有或受保護成員。

所以鎖定字串尤其危險。

下面我們來看看字串駐留是個怎樣的東東。看看下面的程式碼

 1              string a = "lockit";  
 2              string b = "lockit"
 3              string c = "LOCKIT".ToLower();  
 4              string d = "lock" + "it"
 5              string e1 = "lock";
 6              string e2 = "it";
 7              string ee = e1 + "it";
 8              string e = e1 + e2; 
 9              Console.WriteLine(object.ReferenceEquals(a, b));
10              Console.WriteLine(object.ReferenceEquals(a, c));
11              Console.WriteLine(object.ReferenceEquals(a, d));
12              Console.WriteLine(object.ReferenceEquals(a, ee));
13              Console.WriteLine(object.ReferenceEquals(a, e));

 下邊是輸出的結果:

True,False,True,False,False

我們知道CLR處理每個物件在記憶體的時候都會額外的生成一個syncblockindex空間,這個就是用於物件同步用的。是大家平時開發使用最多的一個型別,MS的CLR部門為了簡化操作和效能的優化做了兩點處理,一是將string的建立過程簡單化。一般的物件在建立的時候通過new關鍵字來實現;而string不需要這麼做,我們只需要把對應的字元換賦給給對應的字串變數就可以了。那麼他們在建立過程中使用的MSIL指令時不同的——一般的引用物件的建立是通過newobj這樣一個IL指令來實現的,而建立一個字串變數的IL指令則是ldstrload string)。其次就是為了考慮效能的提升和記憶體節約上,對於建立相同的字串,一般不會為他們分別分配記憶體塊,相反地,他們會共享一塊記憶體。CLR內部維護一個HashTable,這HashTable維護者大部分建立的string。這個HashTableKey對應的相應的string本身,而Value則是分配給這個string的記憶體塊的引用。我們知道在一個託管程序被建立以後,在託管程序的記憶體空間裡面,包含了System DomainShared Domain等等,而這個HashTable就是放在System Domain裡,所以它是在這整個程式的生命週期裡都是存在的和被共享的。一般地,在程式執行過程中,如果需要的建立一個stringCLR會根據這個stringHash Code試著在HashTable中找這個相同的string,如果找到,則直接把找到的string的地址賦給相應的變數,如果沒有則在託管堆中建立一個stringCLR會先在managed heap中建立該strng,並在HashTable中建立一個Key-Value,Key為這個string本身,Valuestring的記憶體地址,這個地址最重被賦給響應的變數。

上面的例子後面兩個為False告訴我們對於對一個動態建立的字串,駐留機制便不會起作用。這種情況產生的MSIL也不一樣。但是我們可以用System.String中的靜態方法Intern來解決。

 

 

公共語言執行庫通過維護一個表來存放字串,該表稱為拘留池,它包含程式中以程式設計方式宣告或建立的每個唯一的字串的一個引用。因此,具有特定值的字串的例項在系統中只有一個。

 

例如,如果將同一字串分配給幾個變數,執行庫就會從拘留池中檢索對該字串的相同引用,並將它分配給各個變數。

 

Intern 方法使用拘留池來搜尋與 str 值相等的字串。如果存在這樣的字串,則返回拘留池中它的引用。如果不存在,則向拘留池新增對 str 的引用,然後返回該引用。

 

在下面的 C# 示例中,值為“MyTest”的字串 s1 已經留用,因為它在程式中是一個字串常量。

 

System.Text.StringBuilder 類生成與 s1 同值的新字串物件。對該字串的引用被分配給 s2。

 

Intern 方法搜尋與 s2 具有相同值的字串。由於存在這樣的字串,該方法會返回分配給 s1 的同一引用,然後將該引用分配給 s3。

 

引用 s1 和 s2 的比較結果是不相等的,這是因為它們引用的是不同的物件;而引用 s1 和 s3 的比較結果是相等的,因為它們引用的是相同的字串。

 

   
 String s1 = "MyTest"; 
 String s2 = new StringBuilder().Append("My").Append("Test").ToString(); 
 String s3 = String.Intern(s2); 
 Console.WriteLine((Object)s2==(Object)s1); // Different references.
 Console.WriteLine((Object)s3==(Object)s1); // The same reference.

 

將此方法與 IsInterned 方法進行比較。