從0自學C#12--線程同步解決方法彙總以及優缺點

首先,肯定的一點:Microsoft的Framework Class Library(FCL)保證了所有靜態方法都是線程安全的。

FCL不保證實例方法是線程安全的。因爲假如全部添加鎖定,會造成性能的巨大損失。另外,假如每個實例方法都需要獲取和釋放一個鎖,事實上會造成最終在任何給定的時刻,你的應用程序只有一個線程在運行,這對性能的影響顯而易見。

下面介紹基元線程同步構造。

基元:是指可以在代碼中使用的最簡單的構造。有兩種基元構造:用戶模式(user-mode)和內核模式(kernel-mode)。

用戶模式

使用了特殊的CPU指令來協調線程。

技術:volatile關鍵字、Interlocked類(互鎖)、spinlock(自旋鎖)

常見鎖①:volatile 關鍵字指示一個字段可以由多個同時執行的線程修改。 聲明爲 volatile 的字段不受編譯器優化(假定由單個線程訪問)的限制。 這樣可以確保該字段在任何時間呈現的都是最新的值。

Interlocked類: 爲多個線程共享的變量提供原子操作。。所謂原子操作是指不會被線程調度機制打斷的操作;這種操作一旦開始,就一直運行到結束,中間不會有任何 context switch (切換到另一個線程)

常見鎖②:SpinLock 結構是一個低級別的互斥同步基元,它在等待獲取鎖時進行旋轉。在多核計算機上,當等待時間預計較短且極少出現爭用情況時,SpinLock 的性能將高於其他類型的鎖。即使 SpinLock 未獲取鎖,它也會產生線程的時間片。 它這樣做是爲了避免線程優先級別反轉,並使垃圾回收器能夠繼續執行。 在使用 SpinLock 時,請確保任何線程持有鎖的時間不會超過一個非常短的時間段,並確保任何線程在持有鎖時不會阻塞。


優點:

應儘量使用基元用戶模式構造,它們的速度要顯著快於內核模式的構造。

  1. 協調線程的在硬件中發生的(所以才這麼快)。
  2. 但是Microsoft Windows操作系統永遠檢測不到一個線程在基元用戶模式的構造上阻塞了。
  3. 由於在用戶模式的基元構造上阻塞的線程池永遠不認爲已堵塞,所以線程池不會創建新線程來替換這種臨時的線程。
  4. 這些CPU指令只阻塞線程相當短的時間。

缺點:

  1. 只有Windows操作系統內核才能停止一個線程的運行(防止它浪費CPU的時間)。
  2. 在用戶模式中運行的線程可能被系統搶佔,但線程會以最快的速度再次調度。
  3. 想要取得資源但暫時取不到的線程會一直在用戶模式中“自旋”,這可能浪費大量的CPU時間。線程一直在一個CPU上運行,我們稱爲“活鎖”(livelock)。

實例:


using System;
using System.Threading;

public class Worker
{
    // This method is called when the thread is started.
    public void DoWork()
    {
        while (!_shouldStop)
        {
            Console.WriteLine("Worker thread: working...");
        }
        Console.WriteLine("Worker thread: terminating gracefully.");
    }
    public void RequestStop()
    {
        _shouldStop = true;
    }
    // Keyword volatile is used as a hint to the compiler that this data
    // member is accessed by multiple threads.
    private volatile bool _shouldStop;
}

public class WorkerThreadExample
{
    static void Main()
    {
        // Create the worker thread object. This does not start the thread.
        Worker workerObject = new Worker();
        Thread workerThread = new Thread(workerObject.DoWork);

        // Start the worker thread.
        workerThread.Start();
        Console.WriteLine("Main thread: starting worker thread...");

        // Loop until the worker thread activates.
        while (!workerThread.IsAlive) ;

        // Put the main thread to sleep for 1 millisecond to
        // allow the worker thread to do some work.
        Thread.Sleep(1);

        // Request that the worker thread stop itself.
        workerObject.RequestStop();

        // Use the Thread.Join method to block the current thread 
        // until the object's thread terminates.
        workerThread.Join();
        Console.WriteLine("Main thread: worker thread has terminated.");
    }
    // Sample output:
    // Main thread: starting worker thread...
    // Worker thread: working...
    // Worker thread: working...
    // Worker thread: working...
    // Worker thread: working...
    // Worker thread: working...
    // Worker thread: working...
    // Worker thread: terminating gracefully.
    // Main thread: worker thread has terminated.
}

內核模式

由Windows操作系統自身提供的。它們要求在應用程序的線程中調用有操作系統內核實現的函數

技術:EventWaitHandle(事件)、Semaphore(信號量)、Mutex(互斥體)

System.Object
  System.MarshalByRefObject
    System.Threading.WaitHandle
      System.Threading.EventWaitHandle
      System.Threading.Mutex
      System.Threading.Semaphore

常見鎖③:Mutex 類是 Win32 構造的包裝,它可以跨應用程序域邊界進行封送處理,可用於多個等待,並且可用於同步不同進程中的線程。


優點:

  1. 線程通過內核模式的構造獲取其他線程擁有的資源時,Windows會阻塞線程以避免它浪費CPU時間。當資源變得可用時,Windows會恢復線程,允許它訪問資源。它不會佔着一個CPU“自旋”。
  2. 可實現本機和託管線程相互之間的同步。
  3. 可同步在同一臺機器的不同進程中運行的線程。
  4. 可應用安全性設置,防止未經授權的賬戶訪問它們。
  5. 線程可一直阻塞,直到及合作的所有內核模式構造都可用,或者直到集合中的任何內核模式構造可用。
  6. 在內核模式的構造上阻塞的線程可指定超時值:指定時間內訪問不到希望的資源,線程就可以解除阻塞並執行其他任務。

缺點:

將線程從用戶模式切換爲內核模式(或者相反)會招致巨大的性能損失,這正是爲什麼要避免使用內核構造的原因。另外,線程一直阻塞,會導致“死鎖“(deadlock)。

死鎖總是由於活鎖,因爲活鎖即浪費CPU時間,有浪費內存(線程棧等),而死鎖只浪費內存。


混合構造

兼具上面兩者的長處。在沒有競爭的情況下,快而且不會阻塞(就像用戶模式)。在有競爭的情況,希望它被操作系統內核阻塞。

技術:ManualResetEventSlim類、SemaphoreSlim類、Monitor類、Lock類、ReaderWriterLockSlim類、CountdownEvent類、Barrier類、雙檢鎖.

常見鎖④:Monitor 通常更爲可取,因爲監視器是專門爲 .NET Framework 而設計的,因而它比Mutex可以更好地利用資源。儘管 mutex 比監視器更爲強大,但是相對於 Monitor 類,它所需要的互操作轉換更消耗計算資源。

常見鎖⑤:使用 lock (C#) 或 SyncLock (Visual Basic) 關鍵字是Monitor的封裝。通常比直接使用 Monitor 類更可取,一方面是因爲 lock 或 SyncLock 更簡潔,另一方面是因爲lock 或 SyncLock 確保了即使受保護的代碼引發異常,也可以釋放基礎監視器。

常見鎖⑥:ReaderWriterLock 鎖,在某些情況下,可能希望只在寫入數據時鎖定資源,在不更新數據時允許多個客戶端同時讀取數據。 ReaderWriterLock 類在線程修改資源時將強制其獨佔訪問資源,但在讀取資源時則允許非獨佔訪問。 ReaderWriter 鎖可用於代替排它鎖。使用排它鎖時,即使其他線程不需要更新數據,也會讓這些線程等待。

雙檢鎖

常見鎖⑦:雙重檢查鎖定模式(也被稱爲”雙重檢查加鎖優化”,”鎖暗示”(Lock hint)) 是一種軟件設計模式用來減少併發系統中競爭和同步的開銷。

雙重檢查鎖定模式首先驗證鎖定條件(第一次檢查),只有通過鎖定條件驗證才真正的進行加鎖邏輯並再次驗證條件(第二次檢查)。

它通常用於減少加鎖開銷,尤其是爲多線程環境中的單例模式實現“惰性初始化”。惰性初始化的意思是直到第一次訪問時才初始化它的值。


public sealed class Singleton
    {
        private static volatile Singleton instance = null;
        private static object syncRoot = new Object();
        private static int count = 100;
        private Singleton() { }
        public static Singleton Instance
        {
            get
            {
                if (instance == null)
                {
                    lock (syncRoot)
                    {
                        if (instance == null)
                            instance = new Singleton();
                    }
                }
                return instance;
            }
        }

        public static Singleton GetSingleton()
        {
            if (instance == null)
            {
                lock (syncRoot)
                {
                    if (instance == null)
                    {
                        instance = new Singleton();
                    }                        
                }
            }
            return instance;
        }

        public override string ToString()
        {
            lock (syncRoot)
            {
                int buf = --count;
                Thread.Sleep(20);
                return buf + "_" + count.ToString();
            }
        }
    }

static void Main(string[] args)
        {
            Task<string>[] tasks = new Task<string>[10];
            Stopwatch watch = new Stopwatch();
            object syncRoot = new Object();
            watch.Start();
            for (int i = 0; i < tasks.Length; i++)
            {
                tasks[i] = Task.Factory.StartNew<string>(() =>(Singleton.GetSingleton().ToString()));                    
            }
            Task.WaitAll(tasks, 5000);//設置超時5s
            watch.Stop();
            Console.WriteLine("Tasks running need " + watch.ElapsedMilliseconds + " ms." + "\n");

            for (int i = 0; i < tasks.Length; i++)
            {
                //超時處理
                if (tasks[i].Status != TaskStatus.RanToCompletion)
                {
                    Console.WriteLine("Task {0} Error!", i + 1);
                }
                else
                {
                    //save result
                    Console.WriteLine("Tick ID is " + tasks[i].Result);
                    Console.WriteLine();
                }
            }

            for (int i = 0; i < 3; i++)
            {
                Console.WriteLine("Main thread do work!");
                Thread.Sleep(200);
            }

            Console.ReadKey();
        }

輸出:

Tasks running need 298 ms.

Tick ID is 96_96

Tick ID is 99_99

Tick ID is 97_97

Tick ID is 98_98

Tick ID is 95_95

Tick ID is 94_94

Tick ID is 93_93

Tick ID is 92_92

Tick ID is 91_91

Tick ID is 90_90

Main thread do work!
Main thread do work!
Main thread do work!
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章