線程同步

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

本篇主要說明線程同步。

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

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

  1. 性能損耗。獲取,釋放鎖,線程上下文建切換都是耗性能的。

  2. 同步會使線程排隊等待執(zhí)行。

    線程同步的幾種方法:

阻塞

當(dāng)線程調(diào)用Sleep,Join,EndInvoke,線程就處于阻塞狀態(tài)(Sleep使調(diào)用線程阻塞,Join、EndInvoke使另外一個線程阻塞),會立即從cpu退出。(阻塞狀態(tài)的線程不消耗cpu)

當(dāng)線程在阻塞和非阻塞狀態(tài)間切換時會消耗幾毫秒時間。

//Joinstatic void Main(){
  Thread t = new Thread (Go);
  Console.WriteLine ("Main方法已經(jīng)運(yùn)行....");  
  t.Start();
  t.Join();//阻塞Main方法
  Console.WriteLine ("Main方法解除阻塞,繼續(xù)運(yùn)行...");
} 
static void Go(){
  Console.WriteLine ("在t線程上運(yùn)行Go方法..."); 
}//Sleepstatic void Main(){
  Console.WriteLine ("Main方法已經(jīng)運(yùn)行....");  
  Thread.CurrentThread.Sleep(3000);//阻塞當(dāng)前線程
  Console.WriteLine ("Main方法解除阻塞,繼續(xù)運(yùn)行...");
} 
 //Task
 static void Main(){
   Task Task1=Task.Run(() => {  
            Console.WriteLine("task方法執(zhí)行..."); 
              Thread.Sleep(1000);
            }); 
   Console.WriteLine(Task1.IsCompleted);             
   Task1.Wait();//阻塞主線程 ,等該Task1完成
   Console.WriteLine(Task1.IsCompleted); 
}

加鎖(lock)

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

同步對象的選擇:

  • 使用引用類型,值類型加鎖時會裝箱,產(chǎn)生一個新的對象。

  • 使用private修飾,使用public時易產(chǎn)生死鎖。(使用lock(this),lock(typeof(實例))時,該類也應(yīng)該是private)。

  • string不能作為鎖對象。

  • 不能在lock中使用await關(guān)鍵字

鎖是否必須是靜態(tài)類型?

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

如果被鎖定的方法不是靜態(tài)的,那么不能使用靜態(tài)類型的鎖,因為被鎖定的方法是屬于實例的,只要該實例調(diào)用鎖定方法不產(chǎn)生損壞就可以,不同實例間是不需要鎖的。這個鎖只鎖該實例的方法,而不是鎖所有實例的方法.*

class ThreadSafe{
 private static object _locker = new object();
 
  void Go()
  {    lock (_locker)
    {
      ......//共享數(shù)據(jù)的操作 (Static Method),使用靜態(tài)鎖確保所有實例排隊執(zhí)行    }
  }private object _locker2=new object();  void GoTo()  {    lock(_locker2)    //共享數(shù)據(jù)的操作,非靜態(tài)方法,是用非靜態(tài)鎖,確保同一個實例的方法調(diào)用者排隊執(zhí)行
  }
}

同步對象可以兼作它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)

互斥鎖是一個互斥的同步對象,同一時間有且僅有一個線程可以獲取它??梢詫崿F(xiàn)進(jìn)程級別上線程的同步。

class Program
    {      //實例化一個互斥鎖
        public static Mutex mutex = new Mutex();        static void Main(string[] args)        {            for (int i = 0; i < 3; i++)
            {              //在不同的線程中調(diào)用受互斥鎖保護(hù)的方法
                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} 準(zhǔn)備釋放互斥鎖", Thread.CurrentThread.Name);            // 釋放互斥鎖
           mut.ReleaseMutex();
           Console.WriteLine("{0} 已經(jīng)釋放互斥鎖", Thread.CurrentThread.Name);
        }
    }

互斥鎖可以在不同的進(jìn)程間實現(xiàn)線程同步

使用互斥鎖實現(xiàn)一個一次只能啟動一個應(yīng)用程序的功能。

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

互斥鎖的帶有三個參數(shù)的構(gòu)造函數(shù)

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

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

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

    信號和句柄

    lockmutex可以實現(xiàn)線程同步,確保一次只有一個線程執(zhí)行。但是線程間的通信就不能實現(xiàn)。如果線程需要相互通信的話就要使用AutoResetEvent,ManualResetEvent,通過信號來相互通信。它們都有兩個狀態(tài),終止?fàn)顟B(tài)和非終止?fàn)顟B(tài)。只有處于非終止?fàn)顟B(tài)時,線程才可以阻塞。

AutoResetEvent

AutoResetEvent 構(gòu)造函數(shù)可以傳入一個bool類型的參數(shù),false表示將AutoResetEvent對象的初始狀態(tài)設(shè)置為非終止。如果為true標(biāo)識終止?fàn)顟B(tài),那么WaitOne方法就不會再阻塞線程了。但是因為該類會自動的將終止?fàn)顟B(tài)修改為非終止,所以,之后再調(diào)用WaitOne方法就會被阻塞。

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

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

class ThreadSafe {  
    static AutoResetEvent autoEvent;  

    static void Main()  
    {  
        //使AutoResetEvent處于非終止?fàn)顟B(tài)
        autoEvent = new AutoResetEvent(false);  

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

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

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

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

}  

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

ManualResetEvent

ManualResetEventAutoResetEvent用法類似。

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

public class ThreadSafe{    //創(chuàng)建一個處于非終止?fàn)顟B(tài)的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新線程的方法已經(jīng)啟動,且被阻塞,調(diào)用Set釋放阻塞線程");

        mre.Set();

        Thread.Sleep(500);
        Console.WriteLine("\n當(dāng)ManualResetEvent處于終止?fàn)顟B(tài)時,調(diào)用由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調(diào)用Reset方法,ManualResetEvent處于非阻塞狀態(tài),此時調(diào)用Wait.One方法的線程再次被阻塞");
  

        mre.Reset();

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

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

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

        Console.WriteLine(name + " 運(yùn)行并調(diào)用WaitOne()");

        mre.WaitOne();

        Console.WriteLine(name + " 結(jié)束");
    }
}//Thread_2 運(yùn)行并調(diào)用WaitOne()//Thread_1 運(yùn)行并調(diào)用WaitOne()//Thread_0 運(yùn)行并調(diào)用WaitOne()//新線程的方法已經(jīng)啟動,且被阻塞,調(diào)用Set釋放阻塞線程//Thread_2 結(jié)束//Thread_1 結(jié)束//Thread_0 結(jié)束//當(dāng)ManualResetEvent處于終止?fàn)顟B(tài)時,調(diào)用由Wait.One方法的多線程,不會被阻塞。//Thread_3 運(yùn)行并調(diào)用WaitOne()//Thread_4 運(yùn)行并調(diào)用WaitOne()//Thread_4 結(jié)束//Thread_3 結(jié)束///調(diào)用Reset方法,ManualResetEvent處于非阻塞狀態(tài),此時調(diào)用Wait.One方法的線程再次被阻塞//Thread_5 運(yùn)行并調(diào)用WaitOne()//調(diào)用Set方法,釋放阻塞線程//Thread_5 結(jié)束

Interlocked

如果一個變量被多個線程修改,讀取??梢杂?code style="margin: 1px 5px; padding: 0px 5px !important; line-height: 1.8; vertical-align: middle; display: inline-block; font-family: "Courier New", sans-serif !important; font-size: 12px !important; background-color: rgb(245, 245, 245) !important; border: 1px solid rgb(204, 204, 204) !important; border-radius: 3px !important;">Interlocked。

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

  1. 將實例變量中的值加載到寄存器中。

  2. 增加或減少該值。

  3. 在實例變量中存儲該值。

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

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

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

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

    location1 和comparand比較,被value替換.

    value 如果第一個參數(shù)和第三個參數(shù)相等,那么就把value賦值給第一個參數(shù)。

    comparand 和第一個參數(shù)對比。

ReaderWriterLock

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

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

    class Program
    {        // 創(chuàng)建一個對象
        public static ReaderWriterLock readerwritelock = new ReaderWriterLock();        static void Main(string[] args)        {            //創(chuàng)建一個線程讀取數(shù)據(jù)
            Thread t1 = new Thread(Write);           // t1.Start(1);
            Thread t2 = new Thread(Write);            //t2.Start(2);
            // 創(chuàng)建10個線程讀取數(shù)據(jù)
            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 + "準(zhǔn)備寫...");
            readerwritelock.AcquireWriterLock(Timeout.Infinite);
            Console.WriteLine("線程:" + i + " 寫操作" + DateTime.Now);            // 釋放寫入鎖
            Console.WriteLine("線程:" + i + "寫結(jié)束...");
            Thread.Sleep(1000);
            readerwritelock.ReleaseWriterLock();

        }        // 讀取方法
        public static void Read(object i)        {
            Console.WriteLine("線程:" + i + "準(zhǔn)備讀...");            // 獲取讀取鎖,20毫秒超時
            readerwritelock.AcquireReaderLock(Timeout.Infinite);
            Console.WriteLine("線程:" + i + " 讀操作" + DateTime.Now);            // 釋放讀取鎖
            Console.WriteLine("線程:" + i + "讀結(jié)束...");
            Thread.Sleep(1000);

            readerwritelock.ReleaseReaderLock();

        }
    }//分別屏蔽writer和reader方法??梢愿逦目吹?nbsp;writer被阻塞了。而reader沒有被阻塞。//屏蔽reader方法//線程:1準(zhǔn)備寫...//線程:1 寫操作2017/7/5 17:50:01//線程:1寫結(jié)束...//線程:2準(zhǔn)備寫...//線程:2 寫操作2017/7/5 17:50:02//線程:2寫結(jié)束...//屏蔽writer方法//線程:3準(zhǔn)備讀...//線程:5準(zhǔn)備讀...//線程:4準(zhǔn)備讀...//線程:5 讀操作2017/7/5 17:50:54//線程:5讀結(jié)束...//線程:3 讀操作2017/7/5 17:50:54//線程:3讀結(jié)束...//線程:4 讀操作2017/7/5 17:50:54

http://www.cnblogs.com/JoeSnail/p/7155815.html