2012-01-19 130 views
5

我知道使用lock(this)或任何共享對象是錯誤的。此鎖使用線程安全嗎?

我不知道這種用法是否可以嗎?

public class A 
{ 
    private readonly object locker = new object(); 
    private List<int> myList; 
    public A() 
    { 
    myList = new List<int>() 
    } 

    private void MethodeA() 
    { 
    lock(locker) 
    { 
     myList.Add(10); 
    } 
    } 

    public void MethodeB() 
    { 
    CallToMethodInOtherClass(myList); 
    } 
} 

public class OtherClass 
{ 
    private readonly object locker = new object(); 
    public CallToMethodInOtherClass(List<int> list) 
    { 
    lock(locker) 
    { 
    int i = list.Count; 
    } 
    } 
} 

此線程安全嗎?在OtherClass我們鎖定一個私人對象,所以如果class A鎖定其私人鎖可以列表仍然在OtherClass鎖塊中更改?

+11

你的浴室有兩扇門,每扇門都有一把鎖。你的問題是「假設我在淋浴時只鎖定第一把鎖,而我的朋友鮑勃在淋浴時只鎖定第二把鎖,我們是否能同時在淋浴中結束?」很明顯是的!如果你和鮑勃想要避免在一起洗澡,那麼你需要同意使用*相同的鎖*。你不能像這樣訪問一個對象線程安全。 –

回答

10

不,它不是線程安全的。添加和計數可以在「相同」時間執行。你有兩個不同的鎖定對象。

通過名單時,請務必鎖定自己的鎖定對象:

public void MethodeB() 
    { 
    lock(locker) 
    { 
     CallToMethodInOtherClass(myList); 
    } 
    } 
+0

它實際上是線程安全的,儘管只是作爲'Count'的實現細節是線程安全的,而不是以承諾的方式進行,並且不能以更好的方式擴展到更真實的代碼。 –

2

不,這不是線程安全的。

你的2個方法鎖定在2個不同的對象上,它們不會互相鎖定。

因爲CallToMethodInOtherClass()只檢索計數的值什麼都不會有可怕的錯誤。但圍繞它的lock()是無用的和誤導。

如果該方法會在列表中進行更改,則會產生令人討厭的問題。要解決它,請更改MethodeB:

public void MethodeB() 
    { 
    lock(locker) // same instance as MethodA is using 
    { 
     CallToMethodInOtherClass(myList); 
    } 
    } 
+0

那是我的想法。那麼你能告訴我如何正確的方式? – Maya

2

不,它們必須鎖定相同的對象。使用您的代碼,它們都會鎖定在不同的位置,並且每個調用都可以同時執行。

爲了使代碼線程安全,請在MethodeB中放置一個鎖或將該列表本身用作鎖對象。

+0

@ Felix我希望它的錯誤與共享對象鎖定,所以鎖定列表本身的錯誤? – Maya

+0

@Maya這是真的,這是錯的。但這是解決所述問題的簡單方法。但無論如何,正確的方法是創建一個線程安全列表或容器,其中包含列表並僅通過容器訪問列表。 –

3

不,這不是線程安全的。 A.MethodeAOtherClass.CallToMethodInOtherClass鎖定在不同的對象上,所以它們不是互斥的。如果您需要保護對列表的訪問權限,請勿將其傳遞給外部代碼,請將其保密。

0

也許這樣的伎倆

public class A 
{ 
    private List<int> myList; 
    public A() 
    { 
    myList = new List<int>() 
    } 

    private void MethodeA() 
    { 
    lock(myList) 
    { 
     myList.Add(10); 
    } 
    } 

    public void MethodeB() 
    { 
    CallToMethodInOtherClass(myList); 
    } 
} 

public class OtherClass 
{ 
    public CallToMethodInOtherClass(List<int> list) 
    { 
    lock(list) 
    { 
    int i = list.Count; 
    } 
    } 
} 
+0

但我認爲它的前鋒鎖定與剪切對象 – Maya

1

屁股所有的答案說的這些都是不同的鎖對象的最簡單方法。

一個簡單的方法是有一個靜態鎖定對象f.ex:

publc class A 
{ 
    public static readonly object lockObj = new object(); 
} 

,並在這兩個類中使用鎖,如:

lock(A.lockObj) 
{ 
} 
3

不,這不是線程安全的。爲了使線程安全,您可以使用鎖定在static對象上,因爲它們是在線程之間共享的,這可能會導致代碼中出現死鎖,但可以通過維護適當的鎖定順序來處理。有一個與lock相關的性能成本,所以明智地使用它。

希望這有助於

+0

這對我有何幫助? – Maya

+0

謝謝,現在我明白了 – Maya

+0

靜態對象是在其他對象之間共享的,所以如果你對此加鎖,它將一次僅被一個對象消耗。閱讀有關[MSDN上的靜態](http://msdn.microsoft.com/zh-cn/library/79b3xss3(v = vs80).aspx)或[MSDN上的鎖定](http://msdn.microsoft.com/zh-cn/ .com/en-us/library/c5kehkcz(v = vs.80).aspx)(_最佳做法是定義一個私有對象進行鎖定,或者一個私有靜態對象變量來保護所有實例通用的數據。 –

0

許多問題的答案都使用靜態只讀鎖提及。

但是,你真的應該儘量避免這種靜態鎖定。在多個線程正在使用靜態鎖定的情況下創建死鎖將很容易。

您可以使用的是.net 4併發集合之一,它們可以代表您提供某些線程同步,因此不需要使用鎖定。

查看System.collections.Concurrent命名空間。 對於本例,您可以使用ConcurrentBag<T>類。

1

它實際上是線程安全的(純粹作爲一個實現細節上Count問題),但:

  1. 線程安全的代碼片段沒有一個線程安全的應用程序中進行。您可以將不同的線程安全操作組合到非線程安全操作中。實際上,很多非線程安全的代碼可以分解爲更小的代碼塊,所有代碼都是線程安全的。

  2. 由於您所希望的原因,它不是線程安全的,這意味着進一步擴展它不會是線程安全的。

此代碼是線程安全的:

public void CallToMethodInOtherClass(List<int> list) 
{ 
    //note we've no locks! 
    int i = list.Count; 
    //do something with i but don't touch list again. 
} 

與任何列表調用它,它會根據該列表的狀態給i的值,無論什麼其他線程取決於。它不會損壞list。它不會給i一個無效的值。

所以,儘管這個代碼也是線程安全的:

public void CallToMethodInOtherClass(List<int> list) 
{ 
    Console.WriteLine(list[93]); // obviously only works if there's at least 94 items 
          // but that's nothing to do with thread-safety 
} 

此代碼不會是線程安全的:

public void CallToMethodInOtherClass(List<int> list) 
{ 
    lock(locker)//same as in the question, different locker to that used elsewhere. 
    { 
    int i = list.Count; 
    if(i > 93) 
     Console.WriteLine(list[93]); 
    } 
} 

在進一步討論之前,兩位我描述爲thread-安全不被承諾是列表的規範。保守編碼會假設它們不是線程安全的,而不是依賴於實現細節,但是我將依賴實現細節,因爲它會影響如何以重要方式使用鎖的問題:

因爲存在在list上運行的代碼首先沒有獲取locker上的鎖,該代碼不會與CallToMethodInOtherClass同時運行。現在,雖然list.Count是線程安全的,而list[93]是踩踏安全的,但是我們依賴第一個的組合確保第二個工作不是線程安全的。由於鎖外的代碼可能會影響list,因此代碼可以撥打RemoveClear,並在Count之間確保list[93]可以正常工作,list[93]被調用。

現在,如果我們知道list只能添加到,那很好,即使同時發生調整大小,我們也將以list[93]這兩個值的結果爲準。如果寫入list[93]的東西是.NET自動寫入的類型(並且int就是這樣一種類型),那麼我們最終會得到舊的或新的,就像我們正確鎖定了我們一樣根據哪個線程首先進入鎖定,獲取舊的或新的。再一次,這是一個實現細節不是一個指定的承諾,我只是想指出如何線程安全性仍然導致非線程安全的代碼。

將此轉向實際代碼。我們不應該假設list.Countlist[93]是線程安全的,因爲我們沒有承諾他們會這樣做並且可能會改變,但即使我們確實有這個承諾,這兩個承諾也不會加起來,線程安全在一起。

重要的是使用相同的鎖來保護可能會相互干擾的代碼塊。因此,請考慮以下保證爲線程安全的變體:

public class ThreadSafeList 
{ 
    private readonly object locker = new object(); 
    private List<int> myList = new List<int>(); 

    public void Add(int item) 
    { 
    lock(locker) 
     myList.Add(item); 
    } 
    public void Clear() 
    { 
    lock(locker) 
     myList.Clear(); 
    } 
    public int Count 
    { 
    lock(locker) 
     return myList.Count; 
    } 
    public int Item(int index) 
    { 
    lock(locker) 
     return myList[index]; 
    } 
} 

此類保證在其所做的一切中都是線程安全的。不依賴於任何實現細節,在這裏沒有任何方法會因爲另一個線程正在對同一個實例做什麼而破壞狀態或給出不正確的結果。下面的代碼仍然沒有工作,雖然:

// (l is a ThreadSafeList visible to multiple threads. 
if(l.Count > 0) 
    Console.WriteLine(l[0]); 

我們保證每個呼叫100%的線程安全的,但我們不能保證相結合,我們不能保證組合。

我們可以做兩件事。我們可以爲組合添加一個方法。像下面的內容將爲許多類專門針對多線程應用設計是共同的:

public bool TryGetItem(int index, out int value) 
{ 
    lock(locker) 
    { 
    if(l.Count > index) 
    { 
     value = l[index]; 
     return true; 
    } 
    value = 0; 
    return false; 
    } 
} 

這使得計數測試並保證是線程安全的單一操作的項目檢索部分。

另外,最經常是我們需要做的,我們有鎖定發生在這裏的操作進行分組的地方:

lock(lockerOnL)//used by every other piece of code operating on l 
    if(l.Count > 0) 
    Console.WriteLine(l[0]); 

當然,這使得內ThreadSafeList多餘的,太浪費了鎖努力,空間和時間。這是大多數類沒有爲其實例成員提供線程安全性的主要原因 - 因爲您無法有效地保護類內成員的調用組,所以這是浪費時間嘗試去除非線程安全承諾是非常明確的和有用的。

回來在你的問題中的代碼:

CallToMethodInOtherClass鎖應該被刪除,除非OtherClass有它的道理的內部鎖定。它不能做出有意義的承諾,它不會以非線程安全的方式進行組合,並且向程序中添加更多鎖定會增加分析它的複雜性,以確保沒有死鎖。

CallToMethodInOtherClass調用應該由相同的鎖來保護這個類中的其他操作:

public void MethodeB() 
{ 
    lock(locker) 
    CallToMethodInOtherClass(myList); 
} 

這時只要CallToMethodInOtherClass不存儲myList某個地方,這可以通過其他線程稍後可以看出, CallToMethodInOtherClass不是線程安全的,因爲只有能夠訪問myList的代碼纔會自行保證不會與myList上的其他操作同時調用它。

兩個重要的事情是:

  1. 當事情被描述爲「線程安全」的,也很瞭解這是由有前途的,因爲有,「根據線程安全的落在不同種類的承諾「而且它本身就意味着」我不會把這個對象變成一個無意義的狀態「,雖然它是一個重要的構件,但它本身並不是很多。

  2. 組操作鎖,用同一把鎖每個組的會影響同一數據,以及守衛訪問對象,這樣有不可能是另一個線程不打球與此有關。

*這是一個非常有限的線程安全定義。在List<T>上調用list[93],其中T是一種將以原子方式編寫和讀取的類型,並且我們不知道它是否實際上至少有94個項目同樣安全,無論是否有其他線程在其上運行。當然,無論在哪種情況下它都會拋出ArgumentOutOfRangeException這一事實並非大多數人認爲「安全」的事實,但我們對多線程的保證與其中一個保持一致。這是我們通過在單線程中檢查Count而不是在多線程情況下得到更強的保證,這導致我將其描述爲不是線程安全的;雖然這個組合仍然不會腐敗的狀態,但它可能會導致我們確信自己無法發生的異常。

+0

使用ConcurrentBag或ConcurrentDictionary代替,那麼您將不必擔心鎖定。 –

+0

@PålThingbø絲毫不真實。首先,雖然這些集合以及我自己的[線程安全字典和集合](https://bitbucket.org/JonHanna/ariadne)和其他一些集合對於它們上的每個單獨操作都是線程安全的,在上面我給出的理由不一定是線程安全的(想想看,'int'是線程安全的,但不會使'++ x'線程安全,因爲三個線程安全的動作必須作爲一個單元不是線程安全的)。當他們想要一個列表時,很少有人指出某人使用了一個包或字典; ... –

+0

@PålThingbø這些類的語義完全不同。我希望儘快發佈一個線程安全列表類,它比上面的答案提供了更好的併發行爲,儘管它仍然是有限的(不是'RemoveAt'或'Insert';我可能會生成一個不同的類太遲了,但是在不鎖定的情況下進行線程安全比其他操作更麻煩),並且它仍然不會使代碼作爲一個整體神奇地使用線程安全,而不是像'ConcurrentDictionary'一樣使用它,同樣是出於原因我給以上。 –