線程安全知多少

1. 如何定義線程安全

線程安全,拆開來看:html

  • 線程:指多線程的應用場景下。
  • 安全:指數據安全。

多線程就不用過多介紹了,相關類型集中在System.Threading命名空間及其子命名空間下。
數據,這裏特指臨界資源
安全,簡單來講就是多線程對某一臨界資源進行併發操做時,其最終的結果應和單線程操做的結果保持一致。好比Parallel線程安全問題就是說的這個現象。編程

2. 如何判斷是否線程安全

在查MSDN是,咱們常常會看到這樣一句話:安全

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.多線程

直譯過來就是說:該類型的公共靜態成員是線程安全的,但其對應的任何實例成員不確保是線程安全的。併發

你可能對這句話仍是丈二和尚摸不着頭腦。我也是。那如今咱們來理一下。less

首先,咱們來理清一下類型和類型成員:
類型是指類自己,類型成員是指類所包含的方法、屬性、字段、索引、委託、事件等。類型成員又分爲靜態成員和實例成員。靜態成員,顧名思義就是static關鍵字修飾的成員。實例成員,就是對類型實例化建立的對象實例才能訪問到的成員。源碼分析

而後,爲何它能夠確保全部的公共靜態成員是線程安全的呢?是由於它必定經過某種機制,去確保了公共靜態成員的線程安全。(這必定是微軟源碼的一個規範)。
那顯而易見,對實例成員,可能因爲沒有了這樣的一個限制,纔會說,不確保實例成員是線程安全的。ui

以上只是我我的的一種猜想。那顯然僅僅是有猜想仍是不夠的,咱們要驗證它。而最直接有力的方法莫過於查源碼了。this

2.1. StopWatch源碼分析

咱們看下System.Diagnostics.StopWatch的源碼實現。pwa

在這個類中,主要有如下幾個公共靜態成員:

  1. public static readonly long Frequency;
  2. public static readonly bool IsHighResolution;
  3. public static Stopwatch StartNew() {//.....}
  4. public static long GetTimestamp() { //....}

首先前兩個公共靜態字段由於被readonly修飾,只讀不可寫,因此是線程安全的。
後面兩個靜態方法由於沒有涉及到對臨界資源的操做,因此也是線程安全的。
那針對這個StopWatch來講,保證線程安全的機制是:

  1. 使用readonly修飾公共靜態字段
  2. 公共靜態方法中不涉及對臨界資源的操做。

2.2. ArrayList源碼分析

咱們再來看下System.Collections.ArrayList的源碼實現。

這個類中,公共靜態成員主要是幾個靜態方法,我簡單列舉一個:

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);
        }

這一個靜態方法主要用來建立只讀列表,由於不涉及到臨界資源的操做,因此線程安全,其餘幾個靜態方法相似。

咱們再來看一個公共實例方法:

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++;
        }

很顯然,對集合進行新增處理時,咱們涉及到對臨界資源**_items**的操做,可是這裏卻沒有任何線程同步機制去確保線程安全。因此其實例成員不確保是線程安全的。

2.3. ConcurrentBag源碼分析

僅有以上兩個例子,不足以驗證咱們的猜想。接下來咱們來看看線程安全集合System.Collections.Concurrent.ConcurrentBag的源碼實現。
首先咱們來看下MSDN中對ConcurrentBag線程安全的描述:

Thread Safety
All public and protected members of ConcurrentBag are thread-safe and may be used concurrently from multiple threads. However, members accessed through one of the interfaces the ConcurrentBag implements, including extension methods, are not guaranteed to be thread safe and may need to be synchronized by the caller.

這裏爲何能夠自信的保證全部public和protected 成員是線程安全的呢?

一樣,咱們仍是來看看對集合進行Add的方法實現:

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鎖來進行add操做,保證了線程安全。

至此,咱們對MSDN上常常出現的對Thread Safety的解釋,就再也不迷糊了。

若是你仔細看了ConcurrentBag關於Thread Safety的描述的話,後面還有一句:
However, members accessed through one of the interfaces the ConcurrentBag implements, including extension methods, are not guaranteed to be thread safe and may need to be synchronized by the caller.
這又是爲何呢,問題就留給你啦。

3. 如何保證線程安全

經過上面分析的幾段源碼,想必咱們內心也有譜了。
要解決線程安全問題,首先,最重要的是看是否存在臨界資源,若是沒有,那麼就不涉及到線程安全的問題。

若是有臨界資源,就須要對臨界資源進行線程同步處理了。而關於線程同步的方式,可參考C#編程總結(三)線程同步

另外在書寫代碼時,爲了不潛在的線程安全問題,對於不須要改動的公共靜態變量,使用readonly修飾不失爲一個很好的方法。

4. 總結

經過以上分析,咱們知道,在多線程的場景下,對於靜態成員和實例成員沒有絕對的線程安全,其關鍵在於是否有臨界資源

相關文章
相關標籤/搜索