1. 如何定義線程安全
線程安全籍救,拆開來看:
- 線程:指多線程的應(yīng)用場(chǎng)景下。
- 安全:指數(shù)據(jù)安全。
多線程就不用過多介紹了,相關(guān)類型集中在System.Threading
命名空間及其子命名空間下各拷。
數(shù)據(jù),這里特指臨界資源闷营。
安全撤逢,簡(jiǎn)單來說就是多線程對(duì)某一臨界資源進(jìn)行并發(fā)操作時(shí),其最終的結(jié)果應(yīng)和單線程操作的結(jié)果保持一致粮坞。比如Parallel線程安全問題就是說的這個(gè)現(xiàn)象。
2. 如何判斷是否線程安全
在查MSDN是初狰,我們經(jīng)常會(huì)看到這樣一句話:
Thread Safety
Public static (Shared in Visual Basic) members of this type are thread safe. Any instance members are not guaranteed to be thread safe.
直譯過來就是說:該類型的公共靜態(tài)成員是線程安全的莫杈,但其對(duì)應(yīng)的任何實(shí)例成員不確保是線程安全的。
你可能對(duì)這句話還是丈二和尚摸不著頭腦奢入。我也是筝闹。那現(xiàn)在我們來理一下。
首先腥光,我們來理清一下類型和類型成員:
類型是指類本身关顷,類型成員是指類所包含的方法、屬性武福、字段议双、索引、委托捉片、事件等平痰。類型成員又分為靜態(tài)成員和實(shí)例成員。靜態(tài)成員伍纫,顧名思義就是static關(guān)鍵字修飾的成員宗雇。實(shí)例成員,就是對(duì)類型實(shí)例化創(chuàng)建的對(duì)象實(shí)例才能訪問到的成員莹规。
然后赔蒲,為什么它可以確保所有的公共靜態(tài)成員是線程安全的呢?是因?yàn)樗欢ㄍㄟ^某種機(jī)制良漱,去確保了公共靜態(tài)成員的線程安全舞虱。(這一定是微軟源碼的一個(gè)規(guī)范)。
那顯而易見母市,對(duì)實(shí)例成員砾嫉,可能由于沒有了這樣的一個(gè)限制,才會(huì)說窒篱,不確保實(shí)例成員是線程安全的焕刮。
以上只是我個(gè)人的一種猜測(cè)舶沿。那顯然僅僅是有猜測(cè)還是不夠的,我們要驗(yàn)證它配并。而最直接有力的方法莫過于查源碼了括荡。
2.1. StopWatch源碼分析
我們看下System.Diagnostics.StopWatch的源碼實(shí)現(xiàn)。
在這個(gè)類中溉旋,主要有以下幾個(gè)公共靜態(tài)成員:
- public static readonly long Frequency;
- public static readonly bool IsHighResolution;
- public static Stopwatch StartNew() {//.....}
- public static long GetTimestamp() { //....}
首先前兩個(gè)公共靜態(tài)字段因?yàn)楸籸eadonly修飾畸冲,只讀不可寫,所以是線程安全的观腊。
后面兩個(gè)靜態(tài)方法因?yàn)闆]有涉及到對(duì)臨界資源的操作邑闲,所以也是線程安全的。
那針對(duì)這個(gè)StopWatch來說梧油,保證線程安全的機(jī)制是:
- 使用readonly修飾公共靜態(tài)字段
- 公共靜態(tài)方法中不涉及對(duì)臨界資源的操作苫耸。
2.2. ArrayList源碼分析
我們?cè)賮砜聪?a target="_blank" rel="nofollow">System.Collections.ArrayList的源碼實(shí)現(xiàn)。
這個(gè)類中儡陨,公共靜態(tài)成員主要是幾個(gè)靜態(tài)方法褪子,我簡(jiǎn)單列舉一個(gè):
public static IList ReadOnly(IList list) {
if (list==null)
throw new ArgumentNullException("list");
Contract.Ensures(Contract.Result<IList>() != null);
Contract.EndContractBlock();
return new ReadOnlyList(list);
}
這一個(gè)靜態(tài)方法主要用來創(chuàng)建只讀列表,因?yàn)椴簧婕暗脚R界資源的操作骗村,所以線程安全嫌褪,其他幾個(gè)靜態(tài)方法類似。
我們?cè)賮砜匆粋€(gè)公共實(shí)例方法:
private Object[] _items;
private int _size;
private int _version;
public virtual int Add(Object value) {
Contract.Ensures(Contract.Result<int>() >= 0);
if (_size == _items.Length) EnsureCapacity(_size + 1);
_items[_size] = value;
_version++;
return _size++;
}
很顯然胚股,對(duì)集合進(jìn)行新增處理時(shí)笼痛,我們涉及到對(duì)臨界資源_items的操作,但是這里卻沒有任何線程同步機(jī)制去確保線程安全琅拌。所以其實(shí)例成員不確保是線程安全的晃痴。
2.3. ConcurrentBag源碼分析
僅有以上兩個(gè)例子,不足以驗(yàn)證我們的猜測(cè)财忽。接下來我們來看看線程安全集合System.Collections.Concurrent.ConcurrentBag的源碼實(shí)現(xiàn)倘核。
首先我們來看下MSDN中對(duì)ConcurrentBag線程安全的描述:
Thread Safety
All public and protected members of ConcurrentBag<T> are thread-safe and may be used concurrently from multiple threads. However, members accessed through one of the interfaces the ConcurrentBag<T> implements, including extension methods, are not guaranteed to be thread safe and may need to be synchronized by the caller.
這里為什么可以自信的保證所有public和protected 成員是線程安全的呢?
同樣即彪,我們還是來看看對(duì)集合進(jìn)行Add的方法實(shí)現(xiàn):
public void Add(T item)
{
// Get the local list for that thread, create a new list if this thread doesn't exist
//(first time to call add)
ThreadLocalList list = GetThreadList(true);
AddInternal(list, item);
}
private ThreadLocalList GetThreadList(bool forceCreate)
{
ThreadLocalList list = m_locals.Value;
if (list != null)
{
return list;
}
else if (forceCreate)
{
// Acquire the lock to update the m_tailList pointer
lock (GlobalListsLock)
{
if (m_headList == null)
{
list = new ThreadLocalList(Thread.CurrentThread);
m_headList = list;
m_tailList = list;
}
else
{
list = GetUnownedList();
if (list == null)
{
list = new ThreadLocalList(Thread.CurrentThread);
m_tailList.m_nextList = list;
m_tailList = list;
}
}
m_locals.Value = list;
}
}
else
{
return null;
}
Debug.Assert(list != null);
return list;
}
/// <summary>
/// </summary>
/// <param name="list"></param>
/// <param name="item"></param>
private void AddInternal(ThreadLocalList list, T item)
{
bool lockTaken = false;
try
{
#pragma warning disable 0420
Interlocked.Exchange(ref list.m_currentOp, (int)ListOperation.Add);
#pragma warning restore 0420
//Synchronization cases:
// if the list count is less than two to avoid conflict with any stealing thread
// if m_needSync is set, this means there is a thread that needs to freeze the bag
if (list.Count < 2 || m_needSync)
{
// reset it back to zero to avoid deadlock with stealing thread
list.m_currentOp = (int)ListOperation.None;
Monitor.Enter(list, ref lockTaken);
}
list.Add(item, lockTaken);
}
finally
{
list.m_currentOp = (int)ListOperation.None;
if (lockTaken)
{
Monitor.Exit(list);
}
}
}
看了源碼紧唱,就一目了然了。首先使用lock鎖獲取臨界資源list
隶校,再使用Moniter鎖來進(jìn)行add操作漏益,保證了線程安全。
至此深胳,我們對(duì)MSDN上經(jīng)常出現(xiàn)的對(duì)Thread Safety的解釋绰疤,就不再迷糊了。
如果你仔細(xì)看了ConcurrentBag關(guān)于Thread Safety的描述的話舞终,后面還有一句:
However, members accessed through one of the interfaces the ConcurrentBag<T> implements, including extension methods, are not guaranteed to be thread safe and may need to be synchronized by the caller.
這又是為什么呢轻庆,問題就留給你啦癣猾。
3. 如何保證線程安全
通過上面分析的幾段源碼,想必我們心里也有譜了余爆。
要解決線程安全問題纷宇,首先,最重要的是看是否存在臨界資源蛾方,如果沒有像捶,那么就不涉及到線程安全的問題。
如果有臨界資源桩砰,就需要對(duì)臨界資源進(jìn)行線程同步處理了拓春。而關(guān)于線程同步的方式,可參考C#編程總結(jié)(三)線程同步亚隅。
另外在書寫代碼時(shí)硼莽,為了避免潛在的線程安全問題,對(duì)于不需要改動(dòng)的公共靜態(tài)變量枢步,使用readonly修飾不失為一個(gè)很好的方法。
4. 總結(jié)
通過以上分析渐尿,我們知道醉途,在多線程的場(chǎng)景下,對(duì)于靜態(tài)成員和實(shí)例成員沒有絕對(duì)的線程安全砖茸,其關(guān)鍵在于是否有臨界資源隘擎。