1. 程式人生 > >C#多執行緒之旅~上車吧?

C#多執行緒之旅~上車吧?

  前言:前幾天,寫了一篇關於多執行緒使用的文章,【執行緒使用】用法得到不少博友的好評,博主這幾天加班寫文章,把剩下的高階使用給寫完,期望可以得到博友的追贊吧,那麼廢話不多說,開始我們的C#高階用法之旅!!

  前面介紹了適合在應用程式中使用執行緒的兩種情況。但是,在一些情況下,建立新執行緒是不利的。在此不會列出不適臺建立新執行緒的所有情況,只是說明一下在什麼情況下不適合建立新執行緒。本節主要介紹兩種情況:第一種情況是執行順序極其重要,第二種情況是程式碼中常見的一個錯誤在迴圈中建立新執行緒。

再次訪問的執行順序

  從上一篇文章的交替執行的執行緒演示可以說明執行緒流是從一個執行緒到另一個執行緒隨機進行的。這看起來是要先執行一個執行緒,在控制檯中顯示十行,再讓一個執行緒顯示十五行,然後返回一個執行緒,執行八行,決定是否使用執行緒的一個常見錯誤是,自以為知道線上程的指定時間要執行多少程式碼。  下面將要使用一個栗子來說明這個問題。看上去T1會先結束,因為它先啟動,但這是錯的,建立一個應用程式,將其物件設定為Main()。構建這幾個示例,會得到不一樣的結果。

   

    public class ExecutionOrder
    {
        static Thread t1;
        static Thread t2;
        public static void WriteFinished(string threadName)
        {
            switch (threadName)
            {
                case "T1": Console.WriteLine(); Console.WriteLine("T1 Finished"); break; case "T2": Console.WriteLine(); Console.WriteLine("T2 Finished"); break; } } public static void MainGo() { t1 = new Thread(new ThreadStart(Increment)); t2 = new Thread(new ThreadStart(Increment)); t1.Name = "T1"; t2.Name = "T2"; t1.Start(); t2.Start(); Console.ReadLine(); } public static void Increment() { for (long i=1;i<=1000000;i++) { if (i%10000==0) { Console.WriteLine("{"+Thread.CurrentThread.Name+"}"); } } WriteFinished(Thread.CurrentThread.Name); } }

  有時候,執行緒t1先會結束,但有的時候t2會先結束,所以我們不能指定執行緒按照啟動的先後順序結束。

初級程式設計師會跳的坑

  嚐到執行緒的甜頭後,人們常犯的另一個錯誤是在一個迴圈中建立並使用執行緒。下面是演示這個錯誤的程式碼示例,這種程式碼通常是由剛剛接觸到執行緒概念的程式設計師編寫的。在發生一個事件時,開發人員或系統管理員通常使用這個概念來發送通知。這個想法不錯,但是在迴圈中使用執行緒實現它會導致很多問題。
 public static void SendAllEmail()
        {
            int loopTo = al.count - 1;
            for (int i=0;i<=loopTo;i++)
            {
                Thread t = LoopingThreads.CreateEmail(
                     new LoopingThreads.SendMail(Mailer.MailMethod,
                     (string)[i], "
[email protected]
", "Thread in a joop", "Mail Example"); t.Start(); t.Join(Timeout.Infinite); } }
  此處省略了很多程式碼,因為這些測試引數非常之多,就不再寫個郵箱了,我們來分析一下程式碼,這乍看上去,這很不錯,為什麼不在另一個執行緒上傳送Email呢?它有時需要很長時間來處理,這是正確的。但問題在於來回切換執行緒而帶來的CPU消耗。到完成這個程序為止。分配到每個執行緒上的時間都是來源於拆包和打包執行緒本地儲存上了。執行執行緒中的指令花費了很少的時間。系統可能完全鎖住,無法傳送郵件。一個通用的程式設計慣例是將工作放到佇列中。由伺服器處理。例如一家銀行將基XML的檔案放到一個網路目錄中,讓在另一臺伺服器上執行的服務程式獲得這個文化這個服務將瀏覽目錄,查詢新的檔案,然後一次處理一個檔案。如果-次將多個文放到目錄中,服務也將依次處理檔案。在一個典型的環境中,新檔案很少放在這個錄中。因此,乍看上去,在找到一個檔案時,應啟動一個新執行緒。這可能是對的,是,如果處理這些檔案的服務程式停止了,會發生什麼情況呢?如果因為網路問題很長時間裡阻止服務訪問這個目錄,會發生什麼情況呢?目錄中的檔案會堆積起來當再次啟動服務時,或者允許服務再次訪問目錄時,每一個檔案都會在服務上產生個新執行緒,這會使伺服器停止執行。   這個檔案模型只是個栗子,要記住的是如果把工作放到佇列中,並且要用多執行緒就應該使用執行緒池。

 為何要使用同步?

.NET 開發人員之所以需要設計多執行緒應用程式時保持同步,主要有兩個原因。
  • 避免競爭條件
  • 確保執行緒的安全
  由於.NET Framework對執行緒提供了內建的支援,所以使所開發的類最終能夠應用於多執行緒應用程式。所設計的每個類不必(也不需要)都是執行緒安全的,因為執行緒安的設計.NET類時,至少要考慮到執行緒安全。本章後面將介紹執行緒安全的代價以及何時使類具有執行緒安全性的指導原則。不必擔心多執行緒對區域性變數、方法引數和返回值的訪問,因為這些變數駐留在堆疊中,本來就具備執行緒安全性。不過,只要很合適地設計類,例項和類變數都將是執行緒安全的。    舉一個ATM的例子。X先生和X夫人決定從ATM.上支取1000,銷掉他們的支票賬號。遺憾的是他們忘記決定由誰來做此事了。更富有戲劇性的是,X先生和X夫人幾乎在同時刻從不同的ATM機上進入他們的支票賬戶。如果兩個使用者同時訪問同一個賬戶,而應用程式不是執行緒安全的,那麼兩個ATM機就有可能發現支票賬戶上有足夠的存量,並分配給每個使用者1000。兩個使用者觸發兩個執行緒同時訪問賬戶資料庫。在理想情況下,當一個使用者試圖修改其賬戶時,其他使用者就不能在同一時刻訪問該賬戶了。簡言之,當用戶訪問賬戶,以修改與這個賬戶相關的資訊時,該賬戶就應被鎖定。所以至少有三種方式來保證物件的執行緒安全:
  • 同步程式碼中的重要部分
  • 使物件不可改變
  • 使用執行緒安全包裝器
我們將要根據這三個方式來續寫下文。

同步重要的程式碼段

  為避免多個執行緒在同一時刻更新資源而引起的不良結果,需要限制對資源的訪問,只允許在同一時刻只有一個執行緒能夠更新資源,換勾話說,就是使資源具有執行緒安全性。使物件或例項變數具有執行緒安全性的最簡單方式是標識和同步重要的程式碼段。重要的程式碼段是指程式中多個執行緒可以在同一時刻訪問以更新物件的狀態的一段程式碼。例如,在上面的例子中,X夫婦二人試圖同時訪問同一個Withdraw()方法,Withdraw()方法就成為重要的程式碼段,需要具有執行緒安全性。最容易實施的方式是同步化方法Withdraw(),以便在任一時刻只有一個執行緒(X先生或X夫人)能夠訪問資源。在執行期間不能被中斷的事務處理叫作原子操作。原子一其傳統的含義是不可分的單元,原子操作的處理是作為一個完整單元執行的程式碼單元一就好像這些程式碼是一條處理器指令一樣,使Withdraw()方法具有原子性一樣,就可以確保在第一個執行緒更改完成之前,另一個執行緒無法去修改同一賬戶的餘額。下面列出的是一個非執行緒安全的Account類的虛擬碼:
public class Account
    {
        public ApprovedOrNot WithDraw(Amount)
        {
            //Check...
        }
    }
下面列出了Account類的執行緒安全的虛擬碼版本:
public class Account
    {
        public ApprovedOrNot Withdraw(Amount)
        {
            lock this section(access for only one thread)
            {
                //check
            }
        }
    }
     在第一段程式碼中,兩個或者兩個以上的執行緒可以同時到達重要的程式碼,因此就有可能兩個執行緒在同一時刻核對賬戶餘額並分發1000美金,導致賬戶的透支。  而在另一段程式碼中,在任何時刻只能允許一個執行緒訪問其重要程式碼,假設X先生來到了銀行,開始進行check來驗證,那麼X女士再來的時候,就不能讓他進行check驗證,也就防止了賬戶透支的情況。

使賬戶物件不可改變

      使物件具有執行緒安全性的另1種方式是使物件不可改變。不可變的物件是指該物件旦建立,其狀態就不能修改。只要在建立Account物件之後,不允許任何執行緒修改其狀態,就可以實現這項操作。在這種方法中,要將讀取例項變數的程式碼段和寫入例項變數的程式碼段區分開。僅對讀取例項變數的程式碼段不做改動,而對修改物件例項變數的程式碼段進行改變,不會修改當前物件的狀態,而是會建立一個包含新狀態的新物件,並返回對新物件的引用。在這種方法中,不需要鎖定程式碼段,因為不可變物件的所有方法(只有建構函式)都不會寫入物件的例項變數,所以,不可變物件在本質上就具有執行緒安全性。

使用執行緒安全包裝器

  使物件具有執行緒安全性的第三個方法是在物件上編寫一個具有執行緒安全性的包裝器,而不是使物件本身具有執行緒安全性,物件保持不變,而新的包裝器包括執行緒安全程式碼的同步部分。下面列出基於Account物件的包裝器類:
public class AccountWrapper
    {
        private Account _a;
        public AccountWrapper(Account a)
        {
            this._a = a;
        }
        public bool Withdraw(double amount)
        {
            lock (_a) { return this._a.WithDraw(amount); } } }
  AccountWrapper類是Account 類的執行緒安全包裝器,Account 例項被宣告為AccountWrapper類的Private例項變數,因此其他物件或執行緒不能訪問Account變數。在這種方法中,Account 物件不具有任何執行緒安全的特性,因為執行緒安全性是由AccountWrapper類提供的。       在處理第三方的庫,而庫中的類不是為執行緒安全而設計時,就可以使用這種方法。例如,假設銀行已經有-一個Account類,用於為它的主機系統開發軟體,為了保持-致,應使用同-一個Account類來編寫ATM軟體。在銀行提供的Account類的文件中,顯然Account類不是執行緒安全的。另外,出於安全的原因,也不能訪問Account的原始碼。在這種情況下,必須採用執行緒安全包裝器的方法,將執行緒安全的Accountwrapper類開發為Account類的擴充套件版。包裝器用於將同步程式碼新增到非執行緒安全的資源中。所有的同步邏輯都在包裝器類中,非執行緒安全的類則保持不變。

.NET 對同步的支援

  .NET Framework在System.Threading、System.EnterpriseServices和System.Runtime.Complier 名稱空間上提供了一些類,當然.NET Core也是如此。程式猿可以通過這些開發執行緒安全。下面我們逐一談一談。

MethodlmplAttribute類

   System.Runtime.CompilerService名稱空間包含的一些屬性將影響CLR在執行期間的行為。MethodlmplAttribute就是這樣的一個屬性,它會告訴CLR方法是如何實現的。MethodlmplAttribute的一個建構函式把MethodImplOptions列舉作為其引數。MethodImplOptions列舉有一個欄位Synchronized,它在任一時間只允許一個執行緒來訪問這個方法。這類似於我們的lock關鍵字,下面說明了這個屬性來同步方法(建立MI.cs):
public class MI
    {
        [MethodImpl(MethodImplOptions.Synchronized)]
        public void DoSomeWorkSync()
        {
            Console.WriteLine("DoSomeWorkSync()"
                               +"--Lock held by Thread"
                               +Thread.CurrentThread.GetHashCode());
            Thread.Sleep(5*1000);
            Console.WriteLine("DoSomeWorkSync()"
                               + "--Lock held by Thread"
                               + Thread.CurrentThread.GetHashCode());
        }
        public void DoSomeWorkNoSync() { Console.WriteLine("DoSomeWorkNoSync()" + "--Lock held by Thread" + Thread.CurrentThread.GetHashCode()); Thread.Sleep(5 * 1000); Console.WriteLine("DoSomeWorkNoSync()" + "--Lock held by Thread" + Thread.CurrentThread.GetHashCode()); } }
class Program
    {
        static void Main(string[] args)
        {
            MI m = new MI();
            Thread t1 = new Thread(new ThreadStart(m.DoSomeWorkNoSync));
            Thread t2 = new Thread(new ThreadStart(m.DoSomeWorkNoSync));
            t1.Start(); t2.Start();
            Thread t3 = new Thread(new ThreadStart(m.DoSomeWorkSync)); Thread t4 = new Thread(new ThreadStart(m.DoSomeWorkSync)); t3.Start(); t4.Start(); } }
上述程式碼的輸出如下所示(計算機不同可能結果就不同,因為程序的ID不同)

      在上面的程式碼中,MI類有兩個方法doSomeWorkSync0和doSomeWorkNoSync()。MethodImpl屬性應用於doSomeWorkSync()方法, 用於同步該方法;而doSomeW orkNoSync()保持不變,允許多個執行緒同時訪問方法。在Main0方法中,執行緒t1和t2訪問未同步的方法,執行緒t3和t4訪問已同步的方法。在這兩個方法中,都添加了Thread,Sleep(方法, 用於為另一個正在等待的執行緒提供足夠的時間進入方法,同時第一個執行緒仍在方法中。如果程式的情況是這樣,執行緒t1和t2就可以同步進入doSomeWorkNoSync()方法,而執行緒t3和t4中只有一個可以進入doSomeWorkSync()方法。如果tl和t2有相同的優先順序,則哪一個執行緒會優先執行就是隨機的; .NETFramework不保證執行緒執行的順序。

      仔細研究一下輸出, 注意執行緒2(t1)和執行緒3(t2)同時進入doSomeW orkNoSync0方法,而"且執行緒4(t3)獲得了doSomeWorkSync()的鎖定,執行緒5(t4)就不能進入該方法了,只有執行緒4(t3)釋放了該方法的鎖定,執行緒5(t4)才能再次獲得該鎖定。

.NET同步策略

公共語言基礎結構提供了3種策略同步去訪問示例、靜態方法和示例欄位,分為以下三種:
  • 同步上下文
  • 同步程式碼區
  • 手控同步

同步上下文

  上下文是一組屬性或使用規則,這種屬性和使用規則對執行時相關的物件集合是通用的。能夠新增的上下文屬性包括有關同步、執行緒親緣性和事務處理的策略,簡而言之,上下文把類似的物件組合在一起,這種策略將使用Synchronization 類對ContextBoundObject物件進行自動同步。由於執行緒同步和併發管理是開發人員遇到最頭疼的事情也是最困難的任務,因此這種方法是極大提高了效率。  Synchronization 類對缺少手工處理同步經驗的程式設計師來說是非常有用的。包括我。因為它包括了例項變數,例項方法和應用這個屬性的類的例項欄位。然而。它不處理靜態欄位和方法的同步。如果必須同步制定程式碼塊。它不起作用。同步整個物件是對輕鬆使用必須付出的代價。在使用System.EnterpriseAttribute程式設計時,Synchronization 非常方便,因為一個上下文的物件由com+執行庫組合在一起。

  回到前面的栗子裡,Account這個示例,可以使用Synchronization 將虛擬碼變成具有安全性的執行緒。下面使用了它進行同步Account類:

 [SynchronizationAttribute(SynchronizationAttribute.REQUIRED)]
    public class Account : ContextBoundObject
    {
        public ApprovedOrNot Withdraw(Amount)
        {
            //check
        }
    }

同步程式碼區

  第二種同步策略是特定程式碼區的同步,這些特定的程式碼區是方法種的重要程式碼段。它們可以改變物件的狀態,或者更新另一資源。下面我們介紹Monitor和ReadWriterLock類。

Monitor類

  Monitor 用於同步程式碼區,其方式是使用Monitor.Enter()方法獲得一個鎖,然後,使用Monitor.Exit()方法釋放該鎖,鎖的概念通常用於解釋Monitor類。一個執行緒獲得鎖。其他執行緒就要等到被釋放之後才可以使用。一旦在程式碼中獲得了鎖,就可以通過Monitor.Enter()和Monitor.Exit()程式塊。

  • Wait() ------ 此方法用於釋放物件上的鎖,並暫停當前執行緒,知道它重新獲得了鎖。

  • Pulse() ------ 此方法用於通知正在佇列中等待的執行緒,物件的狀態已經改變了。

  • PulseAll() ------ 此方法用於通知所有正在佇列中等待的執行緒,物件的狀態已經改變了。

      注意,Monitor 方法是靜態方法,能被Monitor類自身呼叫,而不是由該類的例項呼叫。在.NET Framework中,每個物件都有一個與之相關的鎖,可以獲取和釋放該鎖,這樣,在任一時刻僅有一個執行緒可以訪問物件的例項變數和方法。與之類似,.NETFramework中的每個物件也提供一種允許它處於等待狀態的機制。正如鎖的機制,設計這種機制的主要原因是幫助執行緒間的通訊。如果一個執行緒進入物件的重要程式碼段,並需要一定的條件才能存在,而另一執行緒可以在該程式碼段中建立該條件,此時就需要這種機制,下面是使用Enter()和Exit()方法的示例。

    public class MonitorEnterExit
    {
        private int result = 0;
        public MonitorEnterExit()
        {
        }
        public void NonCriticalSection()
        {
            Console.WriteLine("Entered Thread"+Thread.CurrentThread.ManagedThreadId);
            for (int i=0;i<=5;i++) { Console.WriteLine("Result="+result+++"ThreadID:"+Thread.CurrentThread.GetHashCode()); Thread.Sleep(1000); } Console.WriteLine("Exiting Thread"+Thread.CurrentThread.GetHashCode()); } public void CriticalSection() { //Enter the Critical Section Monitor.Enter(this); Console.WriteLine("Enter Thread"+ Thread.CurrentThread.GetHashCode()); for (int i = 0; i <= 5; i++) { Console.WriteLine("Result=" +result+++ "ThreadID:" + Thread.CurrentThread.GetHashCode()); Thread.Sleep(1000); } Console.WriteLine("Exiting Thread" + Thread.CurrentThread.GetHashCode()); Monitor.Exit(this); } }

上述程式碼的輸出如下所示(計算機不同可能結果就不同,因為程序的ID不同)

  比較一下重要程式碼和非重要程式碼的輸出結果,就會使程式碼變得清晰,兩個執行緒都修改了result變數,產生一個混合變數型輸出結果,這是因為在NonCriticalSection方法中沒有鎖,這個執行緒不是安全的,多個執行緒可以訪問方法。同時區域性變數可以訪問方法。想法我們看一下CriticalSection方法,在第一個執行緒退出之前,其他的執行緒都無法訪問重要的程式碼段,那就這麼鎖住了,怎麼解開呢?請再讀下文!

 Wait()和Pulse()機制

   Wait()和Pulse{)機制用於執行緒間的互動。當在一個物件.上執行Wait()時,正在訪問該物件的執行緒就會進入等待狀態,直到它得到一個喚醒的訊號。Pulse()和PulseAll()用於給等待執行緒傳送訊號。下面列出的是一個Wait{)和Pulse()方法如何工作的例子,即WaitAndPulse.cs示例:

注意:使用這兩個方法一定要在鎖中!!!

public class LockMe{}

    public class WaitPulse1
    {
        private int result = 0;
        private LockMe _IM;
        public WaitPulse1()
        {
        }
        public WaitPulse1(LockMe l) { this._IM = l; } public void CriticalSection() { Monitor.Enter(this._IM); Console.WriteLine("WaitPulsel:Entered Thread" + Thread.CurrentThread.ManagedThreadId); for (int i=1;i<=5;i++) { Monitor.Wait(this._IM); Console.WriteLine("WaitPulsel:WorkUp"); Console.WriteLine("WaitPulsel:Result="+result+++"ThreadID"+Thread.CurrentThread.GetHashCode()); Monitor.Pulse(this._IM); } Console.WriteLine("WaitPulsel:Result=" + result++ + "ThreadID" + Thread.CurrentThread.GetHashCode()); Monitor.Exit(this._IM); } } public class WaitPulse2 { private int result = 0; private LockMe _IM; public WaitPulse2() { } public WaitPulse2(LockMe l) { this._IM = l; } public void CriticalSection() { Monitor.Enter(this._IM); Console.WriteLine("WaitPulse2:Entered Thread" + Thread.CurrentThread.ManagedThreadId); for (int i = 1; i <= 5; i++) { Monitor.Pulse(this._IM); Console.WriteLine("WaitPulse2:Result=" + result++ + "ThreadID" + Thread.CurrentThread.GetHashCode()); Monitor.Pulse(this._IM); Console.WriteLine("WaitPulse2:WorkUp"); } Console.WriteLine("WaitPulse2:Result=" + result++ + "ThreadID" + Thread.CurrentThread.GetHashCode()); Monitor.Exit(this._IM); } }
static void Main(string[] args)
        {
            LockMe lockMe = new LockMe();
            WaitPulse1 el = new WaitPulse1(lockMe);
            WaitPulse2 e2 = new WaitPulse2(lockMe);
            Thread t1 = new Thread(new ThreadStart(el.CriticalSection)); t1.Start();
            Thread t2 = new Thread(new ThreadStart(e2.CriticalSection)); t2.Start(); Console.ReadLine(); }

上述程式碼的輸出如下所示

 

      在Main()方法中,建立一個名為1的LockMe物件。接著建立兩個型別為WaitPulse!和WaitPulse2的物件,然後把它們作為委託傳遞,以便執行緒能夠呼叫這兩個物件的CriticalSection()方法。注意,WaitPulsel 中的LockMe 物件例項與WaitPulse2 中的LockMe物件例項相同,因為物件按引用被傳遞給它們各自的建構函式。初始化物件後,建立兩個執行緒t1和t2,把它們分別傳遞給兩個CriticalSection方法。

      假設WaitPulse1.CriticalSection(首先獲得呼叫,則執行緒t1通過LockMe物件.上的鎖定進入該方法的重要程式碼段,然後在For迴圈中執行Monitor.Wait()。執行Monitor.Wait()方法後,執行緒tl等待另一個執行緒的執行時通知(Monitor.Pulse()),而被喚醒。鎖定LockMe物件是希望在任一時刻都只允許一個執行緒訪問共享的LockMe例項。

      注意,當執行緒執行Monitor. Wait(方法時,它會臨時釋放LockMe物件上的鎖,以便其他執行緒可以訪問它。執行緒t1進入等待狀態後,執行緒t2就可自由訪問LockMe物件。即使LockMe物件是個獨立的物件((WaitPulsel和WaitPulse2),這兩個執行緒也均指向同一個物件引用。執行緒t2獲得LockMe物件上的鎖後,進入WaitPulse2.CriticalSection()方法。它一進入For迴圈,就給在LockMe物件上等待的執行緒(此時是t1)傳送-一個執行時通知(Monitor.Pulse()),之後進入睡眠狀態。結果t1醒來,獲得LockMe物件上的鎖。接著執行緒t1訪問result變數,並給在LockMe物件.上等待的執行緒(此時是t2)傳送-一個執行時通知。這個迴圈一直持續到For迴圈結束。.

      如果把程式的輸出結果和上面的描述相對比,概念就非常明確了。注意每個Enter()方法都應該伴隨一個Exit()方法,否則程式將陷入死迴圈。

TryEnter()方法

  Monitor類的TryEnter()方法非常類似於Enter()方法,它檢視獲得物件的獨佔樓,不過它不會想Enter()方法那樣暫停。如果執行緒成功進入,則TryEnter()方法返回True。
public class MonitorTryEnter
    {
        public MonitorTryEnter()
        {
        }
        public void CriticalSection()
        {
            bool b = Monitor.TryEnter(this,1000);
            Console.WriteLine("Thread=" 
                + Thread.CurrentThread.GetHashCode()+"TryEnter Value"+b);
            for (int i=1;i<=3;i++) { Thread.Sleep(1000); Console.WriteLine(i+" "+Thread.CurrentThread.GetHashCode()+" "); } Monitor.Exit(this); } }

上述程式碼的輸出如下所示

  有可能會發生衝突,那也是無法避免的,如果真的需要知道是否被鎖定,也只能這麼幹。現在到了這裡,我覺得你已經寫的不耐煩了,包括我。。。現在我們使用lock關鍵字,這個關鍵字替代了Monitor,和上面部分程式碼是等價的。

Monitor.Enter(x) ... Monitor.Exit(x) == lock(x){...}
private int result = 0;
        public void CriticalSection()
        {
            lock (this)
            {
                Console.WriteLine("Result=" + result++ + "ThreadID" + Thread.CurrentThread.GetHashCode());
                for (int i=1;i<=5;i++)
                {
                    Console.WriteLine("Result=" + result++ + "ThreadID" + Thread.CurrentThread.GetHashCode()); Thread.Sleep(1000); } Console.WriteLine("Exiting Thread"+ Thread.CurrentThread.GetHashCode()); } }

這是一個最基本的lock用法,除此之外還有一個ReaderWriterLock類

ReadWriteLock

  ReaderWriterLock 定義了實現單寫程式和多讀程式語義的鎖,這個類主要用於檔案操作,即多個執行緒可以讀取檔案,但只能一個執行緒進行更新檔案。ReaderWriterLock 類中4個主要方法是:

  • AcquireReaderLock():該過載方法獲得一個讀程式鎖,超時值使用整數或TimeSpan。超時是用於檢測死鎖的好工具。
  • AcquireWriterLock():該過載方法獲得了一個寫程式鎖,超時值使用整數或TimeSpan。
  • ReleaseReaderLock():釋放讀程式鎖
  • ReleaseWriterLock():釋放寫程式鎖
  使用ReaderWriterLock 類時,任意數量的執行緒都可以同時安全地讀取資料。只有當執行緒進行更新時,資料才被鎖定。只有在沒有佔用鎖的寫程式執行緒時,讀程式執行緒才能獲得鎖。只有在沒有佔用鎖的讀程式或寫程式執行緒時,寫程式執行緒才能獲得鎖。
 下面給出程式ReadWriteLock.cs,展示它們的用法。  
    public class ReadWrite
    {
        private ReaderWriterLock rwl;
        private int x;
        private int y;
        public ReadWrite()
        {
            rwl = new ReaderWriterLock(); } public void ReadInts(ref int a,ref int b) { rwl.AcquireReaderLock(Timeout.Infinite); try { a = this.x; b = this.y; } finally { rwl.ReleaseReaderLock(); } } public void WriteInts(int a,int b) { rwl.AcquireWriterLock(Timeout.Infinite); try { this.x = a; this.y = b; System.Console.WriteLine("x="+this.x+"y="+this.y+ "ThreadID="+Thread.CurrentThread.GetHashCode()); } finally { rwl.ReleaseWriterLock(); } } }
    class Program
    {
        public ReadWrite rw = new ReadWrite();
        static void Main(string[] args)
        {
            Program program = new Program();
            //writer thraeds
            Thread wt1 = new Thread(new ThreadStart(program.Writer));wt1.Start();
            Thread wt2 = new Thread(new ThreadStart(program.Writer));wt2.Start(); //reader threads Thread rt1 = new Thread(new ThreadStart(program.Read));rt1.Start(); Thread rt2 = new Thread(new ThreadStart(program.Read));rt2.Start(); } private void Writer() { int a = 10; int b = 11; Console.WriteLine("*****write*****"); for (int i = 0; i < 5; i++) { this.rw.WriteInts(a++,b++); Thread.Sleep(1000); } } private void Read() { int a = 10; int b = 11; Console.WriteLine("*****raed*****"); for (int i=0;i<5;i++) { this.rw.ReadInts(ref a,ref b); System.Console.WriteLine("x=" + a + "y=" + b + "ThreadID=" + Thread.CurrentThread.GetHashCode()); Thread.Sleep(1000); } } }
 上述程式碼的輸出如下所示

  監控器對於僅計劃讀取資料而不修改資料的執行緒來說可能“太安全”了。在這方面,監控器的效能有所下降,而且,對於只讀型別的訪問,這種效能的降低不是必需的。ReaderWriterLock類允許任意多的執行緒同時讀取資料,提供了一種處理資料讀寫訪問的最佳解決方案。僅當執行緒更新資料時,它才鎖定資料。當且僅當沒有寫程式執行緒佔用鎖時,讀程式執行緒才能獲得鎖。當且僅當沒有讀程式或寫程式執行緒佔用鎖時,寫程式執行緒才能獲得鎖。因此,ReaderWriterLock 類與重要程式碼段是一樣的。除此之外,該類還支援一個可用於檢測死鎖的超時值。

手控同步

  第三個策略用到了手控同步,而.NET Framework提供了一套經典的技術,允許程式設計師類似於win32執行緒API的低階API建立和管理執行緒。

ManualResetEvent 類

  ManualResetEvent物件只能擁有兩種狀態之一。有訊號或者無訊號,是個bool值。它繼承於WaitHandle 類,其建構函式的引數可確定物件的初始狀態。Set()和Reset()方法返回一個bool值,表示是否成功修改。下面列出了相關栗子。

static void Main(string[] args)
        {
            ManualResetEvent manual;
            manual = new ManualResetEvent(false);
            Console.WriteLine("稍等會!");
            bool b = manual.WaitOne(1000,false);
            Console.WriteLine("我靠我來了!"+b);
        }
上述程式碼的輸出如下所示

  程式塊在WaitOne()方法中暫停一秒,然後因為超時而退出,ManualResetEvent 的狀態仍然是false,因而返回的值是false,現在我們把它改成true,試試瞧!!!

static void Main(string[] args)
        {
            ManualResetEvent manual;
            manual = new ManualResetEvent(true);
            Console.WriteLine("稍等會!");
            bool b = manual.WaitOne(1000,true);
            Console.WriteLine("我靠我來了!"+b);
        }

      將ManualResetEvent的初始狀態修改為有訊號的,即使指定超時值為1000亳秒,執行緒在WaitOne()中也不會等待。當ManualResetEvent的狀態為無訊號狀態時,執行緒將等待狀態變為有訊號狀態,但1000毫秒後執行緒超時。狀態已經是有訊號的,  因而執行緒沒理由在WaitOne()方法中等待。為了把ManualResetEvent的狀態修改為無訊號的,必須呼叫ManualResetEvent的Reset()方法;為了把狀態修改為有訊號的,必須呼叫Set()方法。
   下面列出Reset()方法的使用,再舉個栗子(實在寫不動了,累了,明天寫...)
      好,經過了一晚上的休息,今天早上繼續幹!。。。
[STAThread]
        static void Main(string[] args)
        {
            ManualResetEvent manualRE;
            manualRE = new ManualResetEvent(true);
            bool b = manualRE.WaitOne(1000,true);
            Console.WriteLine("稍等會"+b);
            manualRE.Reset();
            b = manualRE.WaitOne(5000,true); Console.WriteLine("中了不" + b); }
 上述程式碼的輸出如下所示    在ManualReset 中,ManualResetEvent 物件的建構函式將其狀態設定為有訊號的(True),結果,執行緒不在第一個WaitOne()方法中 等待,並返回True 值。接著將ManualResetEvent物件的狀態重新設定為無訊號的(False),於是執行緒在超時之前必須等待5秒鐘。
  我們再試一下Set()方法。
[STAThread]
        static void Main(string[] args)
        {
            ManualResetEvent manualRE;
            manualRE = new ManualResetEvent(true);
            bool b = manualRE.WaitOne(1000,true);
            Console.WriteLine("稍等會"+b);
            manualRE.Set();
            b = manualRE.WaitOne(5000,true); Console.WriteLine("中了不" + b); }

WaitOne是等待一個事件有訊號,拿WaitAll是等待所有時間物件都有訊息才可以的。這裡就不再演示了。

AutoResetEvent 類

AutoResetEvent和ManualResetEvent類差不多,它等待時間超時或者事件變成有訊號狀態,接著將此事件通知給等待執行緒。說白了這倆的區別就是,咱們這個的WaitOne方法是直接改變狀態的了。呵呵。

[STAThread]
        static void Main(string[] args)
        {
            AutoResetEvent aRe;
            aRe = new AutoResetEvent(true);
            Console.WriteLine("Before First WaitOne");
            bool state = aRe.WaitOne(1000,true);
            Console.WriteLine("After First WaitOne"+state); state = aRe.WaitOne(5000,true); Console.WriteLine("After Second WaitOne" + state); }

同步和效能

      要獲得同步鎖,就需要一定的系統時間。其結果是,效能總是弱於非執行緒安全版本。當許多執行緒試圖同時訪問物件以獲得同步鎖時,整個應用系統的效能就會受到影響。當設計大型應用程式時,開發人員必須權衡這一點。重要的是,在執行一個徹底的測試之前,這些執行緒之間的競爭是不可見的。在設計大規模的多執行緒應用程式時,測試是非常重要的。開發人員必須權衡這些因素:

      ●為安全起見,儘可能實現同步。這將使程式更慢,更糟糕的是,還沒有單執行緒快。。

      ●為了效能,儘可能減少同步。

   儘管執行緒安全性非常重要,但是如果不使用地使用同步,就可能會引起死鎖,如果避免死鎖和什麼是死鎖,還是要談一談的。例如程式中的迴圈就可能會引起死鎖。