c#線程-線程同步


線程同步

上一篇介紹了如何開啟線程,線程間相互傳遞參數,及線程中本地變量和全局共享變量區別。

本篇主要說明線程同步

如果有多個線程同時訪問共享數據的時候,就必須要用線程同步,防止共享數據被破壞。如果多個線程不會同時訪問共享數據,可以不用線程同步。

線程同步也會有一些問題存在:

  1. 性能損耗。獲取,釋放鎖,線程上下文建切換都是耗性能的。
  2. 同步會使線程排隊等待執行。

線程同步的幾種方法:

阻塞

當線程調用Sleep,Join,EndInvoke,線程就處於阻塞狀態(Sleep使調用線程阻塞,Join、EndInvoke使另外一個線程阻塞),會立即從cpu退出。(阻塞狀態的線程不消耗cpu)

當線程在阻塞和非阻塞狀態間切換時會消耗幾毫秒時間。

//Join
static void Main()
{
  Thread t = new Thread (Go);
  Console.WriteLine ("Main方法已經運行....");  
  t.Start();
  t.Join();//阻塞Main方法
  Console.WriteLine ("Main方法解除阻塞,繼續運行...");
}
 
static void Go()
{
  Console.WriteLine ("在t線程上運行Go方法..."); 
}

//Sleep
static void Main()
{
  Console.WriteLine ("Main方法已經運行....");  
  Thread.CurrentThread.Sleep(3000);//阻塞當前線程
  Console.WriteLine ("Main方法解除阻塞,繼續運行...");
}
 
 //Task
 static void Main()
{
   Task Task1=Task.Run(() => {  
            Console.WriteLine("task方法執行..."); 
              Thread.Sleep(1000);
            }); 
   Console.WriteLine(Task1.IsCompleted);             
   Task1.Wait();//阻塞主線程 ,等該Task1完成
   Console.WriteLine(Task1.IsCompleted); 
}

加鎖(lock)

加鎖使多個線程同一時間只有一個線程可以調用該方法,其他線程被阻塞。

同步對象的選擇:

  • 使用引用類型,值類型加鎖時會裝箱,產生一個新的對象。
  • 使用private修飾,使用public時易產生死鎖。(使用lock(this),lock(typeof(實例))時,該類也應該是private)
  • string不能作為鎖對象。
  • 不能在lock中使用await關鍵字

鎖是否必須是靜態類型?

如果被鎖定的方法是靜態的,那么這個鎖必須是靜態類型。這樣就是在全局鎖定了該方法,不管該類有多少個實例,都要排隊執行。

如果被鎖定的方法不是靜態的,那么不能使用靜態類型的鎖,因為被鎖定的方法是屬於實例的,只要該實例調用鎖定方法不產生損壞就可以,不同實例間是不需要鎖的。這個鎖只鎖該實例的方法,而不是鎖所有實例的方法.*

class ThreadSafe
{
 private static object _locker = new object();
 
  void Go()
  {
    lock (_locker)
    {
      ......//共享數據的操作 (Static Method),使用靜態鎖確保所有實例排隊執行
    }
  }

private object _locker2=new object();
  void GoTo()
  {
    lock(_locker2)
    //共享數據的操作,非靜態方法,是用非靜態鎖,確保同一個實例的方法調用者排隊執行
  }
}

同步對象可以兼作它lock的對象
如:

class ThreadSafe
{
 private List <string> _list = new List <string>(); 
  void Test()
  {
    lock (_list)
    {
      _list.Add ("Item 1");
    }
  }
}

Monitors

lock其實是Monitors的簡潔寫法。

lock (x)  
{  
    DoSomething();  
}  

兩者其實是一樣的。

System.Object obj = (System.Object)x;  
System.Threading.Monitor.Enter(obj);  
try  
{  
    DoSomething();  
}  
finally  
{  
    System.Threading.Monitor.Exit(obj);  
} 

互斥鎖(Mutex)

互斥鎖是一個互斥的同步對象,同一時間有且僅有一個線程可以獲取它。可以實現進程級別上線程的同步。

class Program
    {
      //實例化一個互斥鎖
        public static Mutex mutex = new Mutex();

        static void Main(string[] args)
        {
            for (int i = 0; i < 3; i++)
            {
              //在不同的線程中調用受互斥鎖保護的方法
                Thread test = new Thread(MutexMethod);
                test.Start();
            }
            Console.Read();
        }

        public static void MutexMethod()
        {
           Console.WriteLine("{0} 請求獲取互斥鎖", Thread.CurrentThread.Name);
           mut.WaitOne();
           Console.WriteLine("{0} 已獲取到互斥鎖", Thread.CurrentThread.Name);     
           Thread.Sleep(1000);
           Console.WriteLine("{0} 准備釋放互斥鎖", Thread.CurrentThread.Name);
            // 釋放互斥鎖
           mut.ReleaseMutex();
           Console.WriteLine("{0} 已經釋放互斥鎖", Thread.CurrentThread.Name);
        }
    }

互斥鎖可以在不同的進程間實現線程同步

使用互斥鎖實現一個一次只能啟動一個應用程序的功能。

    public static class SingleInstance
    {
        private static Mutex m;

        public static bool IsSingleInstance()
        {
            //是否需要創建一個應用
            Boolean isCreateNew = false;
            try
            {
               m = new Mutex(initiallyOwned: true, name: "SingleInstanceMutex", createdNew: out isCreateNew);
            }
            catch (Exception ex)
            {
               
            }
            return isCreateNew;
        }
    }

互斥鎖的帶有三個參數的構造函數

  1. initiallyOwned: 如果initiallyOwned為true,互斥鎖的初始狀態就是被所實例化的線程所獲取,否則實例化的線程處於未獲取狀態。

  2. name:該互斥鎖的名字,在操作系統中只有一個命名為name的互斥鎖mutex,如果一個線程得到這個name的互斥鎖,其他線程就無法得到這個互斥鎖了,必須等待那個線程對這個線程釋放。

  3. createNew:如果指定名稱的互斥體已經存在就返回false,否則返回true。


信號和句柄

lockmutex可以實現線程同步,確保一次只有一個線程執行。但是線程間的通信就不能實現。如果線程需要相互通信的話就要使用AutoResetEvent,ManualResetEvent,通過信號來相互通信。它們都有兩個狀態,終止狀態和非終止狀態。只有處於非終止狀態時,線程才可以阻塞。

AutoResetEvent

AutoResetEvent 構造函數可以傳入一個bool類型的參數,false表示將AutoResetEvent對象的初始狀態設置為非終止。如果為true標識終止狀態,那么WaitOne方法就不會再阻塞線程了。但是因為該類會自動的將終止狀態修改為非終止,所以,之后再調用WaitOne方法就會被阻塞。

WaitOne 方法如果AutoResetEvent對象狀態非終止,則阻塞調用該方法的線程。可以指定時間,若沒有獲取到信號,返回false

set 方法釋放被阻塞的線程。但是一次只可以釋放一個被阻塞的線程。

class ThreadSafe 
{  
    static AutoResetEvent autoEvent;  

    static void Main()  
    {  
        //使AutoResetEvent處於非終止狀態
        autoEvent = new AutoResetEvent(false);  

        Console.WriteLine("主線程運行...");  
        Thread t = new Thread(DoWork);  
        t.Start();  

        Console.WriteLine("主線程sleep 1秒...");  
        Thread.Sleep(1000);  

        Console.WriteLine("主線程釋放信號...");  
        autoEvent.Set();  
    }  

     static void DoWork()  
    {  
        Console.WriteLine("  t線程運行DoWork方法,阻塞自己等待main線程信號...");  
        autoEvent.WaitOne();  
        Console.WriteLine("  t線程DoWork方法獲取到main線程信號,繼續執行...");  
    }  

}  

//輸出
//主線程運行...
//主線程sleep 1秒...
//  t線程運行DoWork方法,阻塞自己等待main線程信號...
//主線程釋放信號...
//  t線程DoWork方法獲取到main線程信號,繼續執行...

ManualResetEvent

ManualResetEventAutoResetEvent用法類似。

AutoResetEvent在調用了Set方法后,會自動的將信號由釋放(終止)改為阻塞(非終止),一次只有一個線程會得到釋放信號。而ManualResetEvent在調用Set方法后不會自動的將信號由釋放(終止)改為阻塞(非終止),而是一直保持釋放信號,使得一次有多個被阻塞線程運行,只能手動的調用Reset方法,將信號由釋放(終止)改為阻塞(非終止),之后的再調用Wait.One方法的線程才會被再次阻塞。

public class ThreadSafe
{
    //創建一個處於非終止狀態的ManualResetEvent
    private static ManualResetEvent mre = new ManualResetEvent(false);

    static void Main()
    {
        for(int i = 0; i <= 2; i++)
        {
            Thread t = new Thread(ThreadProc);
            t.Name = "Thread_" + i;
            t.Start();
        }

        Thread.Sleep(500);
        Console.WriteLine("\n新線程的方法已經啟動,且被阻塞,調用Set釋放阻塞線程");

        mre.Set();

        Thread.Sleep(500);
        Console.WriteLine("\n當ManualResetEvent處於終止狀態時,調用由Wait.One方法的多線程,不會被阻塞。");

        for(int i = 3; i <= 4; i++)
        {
            Thread t = new Thread(ThreadProc);
            t.Name = "Thread_" + i;
            t.Start();
        }

        Thread.Sleep(500);
        Console.WriteLine("\n調用Reset方法,ManualResetEvent處於非阻塞狀態,此時調用Wait.One方法的線程再次被阻塞");
  

        mre.Reset();

        Thread t5 = new Thread(ThreadProc);
        t5.Name = "Thread_5";
        t5.Start();

        Thread.Sleep(500);
        Console.WriteLine("\n調用Set方法,釋放阻塞線程");

        mre.Set();
    }


    private static void ThreadProc()
    {
        string name = Thread.CurrentThread.Name;

        Console.WriteLine(name + " 運行並調用WaitOne()");

        mre.WaitOne();

        Console.WriteLine(name + " 結束");
    }
}


//Thread_2 運行並調用WaitOne()
//Thread_1 運行並調用WaitOne()
//Thread_0 運行並調用WaitOne()

//新線程的方法已經啟動,且被阻塞,調用Set釋放阻塞線程

//Thread_2 結束
//Thread_1 結束
//Thread_0 結束

//當ManualResetEvent處於終止狀態時,調用由Wait.One方法的多線程,不會被阻塞。

//Thread_3 運行並調用WaitOne()
//Thread_4 運行並調用WaitOne()

//Thread_4 結束
//Thread_3 結束

///調用Reset方法,ManualResetEvent處於非阻塞狀態,此時調用Wait.One方法的線程再次被阻塞

//Thread_5 運行並調用WaitOne()
//調用Set方法,釋放阻塞線程
//Thread_5 結束


Interlocked

如果一個變量被多個線程修改,讀取。可以用Interlocked

計算機上不能保證對一個數據的增刪是原子性的,因為對數據的操作也是分步驟的:

  1. 將實例變量中的值加載到寄存器中。
  2. 增加或減少該值。
  3. 在實例變量中存儲該值。

Interlocked為多線程共享的變量提供原子操作。
Interlocked提供了需要原子操作的方法:

  • public static int Add (ref int location1, int value); 兩個參數相加,且把結果和賦值該第一個參數。

  • public static int Increment (ref int location); 自增。

  • public static int CompareExchange (ref int location1, int value, int comparand);

    location1 和comparand比較,被value替換.
    
    value 如果第一個參數和第三個參數相等,那么就把value賦值給第一個參數。
    
    comparand 和第一個參數對比。
    

ReaderWriterLock

如果要確保一個資源或數據在被訪問之前是最新的。那么就可以使用ReaderWriterLock.該鎖確保在對資源獲取賦值或更新時,只有它自己可以訪問這些資源,其他線程都不可以訪問。即排它鎖。但用改鎖讀取這些數據時,不能實現排它鎖。

lock允許同一時間只有一個線程執行。而ReaderWriterLock允許同一時間有多個線程可以執行讀操作,或者只有一個有排它鎖的線程執行寫操作

    class Program
    {
        // 創建一個對象
        public static ReaderWriterLock readerwritelock = new ReaderWriterLock();
        static void Main(string[] args)
        {
            //創建一個線程讀取數據
            Thread t1 = new Thread(Write);
           // t1.Start(1);
            Thread t2 = new Thread(Write);
            //t2.Start(2);
            // 創建10個線程讀取數據
            for (int i = 3; i < 6; i++)
            {
                Thread t = new Thread(Read);
              //  t.Start(i);
            }

            Console.Read();

        }

        // 寫入方法
        public static void Write(object i)
        {
            // 獲取寫入鎖,20毫秒超時。
            Console.WriteLine("線程:" + i + "准備寫...");
            readerwritelock.AcquireWriterLock(Timeout.Infinite);
            Console.WriteLine("線程:" + i + " 寫操作" + DateTime.Now);
            // 釋放寫入鎖
            Console.WriteLine("線程:" + i + "寫結束...");
            Thread.Sleep(1000);
            readerwritelock.ReleaseWriterLock();

        }

        // 讀取方法
        public static void Read(object i)
        {
            Console.WriteLine("線程:" + i + "准備讀...");

            // 獲取讀取鎖,20毫秒超時
            readerwritelock.AcquireReaderLock(Timeout.Infinite);
            Console.WriteLine("線程:" + i + " 讀操作" + DateTime.Now);
            // 釋放讀取鎖
            Console.WriteLine("線程:" + i + "讀結束...");
            Thread.Sleep(1000);

            readerwritelock.ReleaseReaderLock();

        }
    }
//分別屏蔽writer和reader方法。可以更清晰的看到 writer被阻塞了。而reader沒有被阻塞。

//屏蔽reader方法
//線程:1准備寫...
//線程:1 寫操作2017/7/5 17:50:01
//線程:1寫結束...
//線程:2准備寫...
//線程:2 寫操作2017/7/5 17:50:02
//線程:2寫結束...

//屏蔽writer方法
//線程:3准備讀...
//線程:5准備讀...
//線程:4准備讀...
//線程:5 讀操作2017/7/5 17:50:54
//線程:5讀結束...
//線程:3 讀操作2017/7/5 17:50:54
//線程:3讀結束...
//線程:4 讀操作2017/7/5 17:50:54
//線程:4讀結束...

參考:

  • MSDN
  • 《CLR via C#》


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2026 CODEPRJ.COM