它實際上是線程安全的(純粹作爲一個實現細節上Count
問題),但:
線程安全的代碼片段沒有一個線程安全的應用程序中進行。您可以將不同的線程安全操作組合到非線程安全操作中。實際上,很多非線程安全的代碼可以分解爲更小的代碼塊,所有代碼都是線程安全的。
由於您所希望的原因,它不是線程安全的,這意味着進一步擴展它不會是線程安全的。
此代碼是線程安全的:
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
,因此代碼可以撥打Remove
或Clear
,並在Count
之間確保list[93]
可以正常工作,list[93]
被調用。
現在,如果我們知道list
只能添加到,那很好,即使同時發生調整大小,我們也將以list[93]
這兩個值的結果爲準。如果寫入list[93]
的東西是.NET自動寫入的類型(並且int
就是這樣一種類型),那麼我們最終會得到舊的或新的,就像我們正確鎖定了我們一樣根據哪個線程首先進入鎖定,獲取舊的或新的。再一次,這是一個實現細節不是一個指定的承諾,我只是想指出如何線程安全性仍然導致非線程安全的代碼。
將此轉向實際代碼。我們不應該假設list.Count
和list[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
上的其他操作同時調用它。
兩個重要的事情是:
當事情被描述爲「線程安全」的,也很瞭解這是由有前途的,因爲有,「根據線程安全的落在不同種類的承諾「而且它本身就意味着」我不會把這個對象變成一個無意義的狀態「,雖然它是一個重要的構件,但它本身並不是很多。
上組操作鎖,用同一把鎖每個組的會影響同一數據,以及守衛訪問對象,這樣有不可能是另一個線程不打球與此有關。
*這是一個非常有限的線程安全定義。在List<T>
上調用list[93]
,其中T
是一種將以原子方式編寫和讀取的類型,並且我們不知道它是否實際上至少有94個項目同樣安全,無論是否有其他線程在其上運行。當然,無論在哪種情況下它都會拋出ArgumentOutOfRangeException
這一事實並非大多數人認爲「安全」的事實,但我們對多線程的保證與其中一個保持一致。這是我們通過在單線程中檢查Count
而不是在多線程情況下得到更強的保證,這導致我將其描述爲不是線程安全的;雖然這個組合仍然不會腐敗的狀態,但它可能會導致我們確信自己無法發生的異常。
你的浴室有兩扇門,每扇門都有一把鎖。你的問題是「假設我在淋浴時只鎖定第一把鎖,而我的朋友鮑勃在淋浴時只鎖定第二把鎖,我們是否能同時在淋浴中結束?」很明顯是的!如果你和鮑勃想要避免在一起洗澡,那麼你需要同意使用*相同的鎖*。你不能像這樣訪問一個對象線程安全。 –