DotNet同步策略

公共語言運行庫(CLR)提供了三種策略去同步訪問實現,靜態方法和實例字段,即:
同步上下文;
同步代碼區;
手控同步.
1 同步上下文
     上下文是一組屬性或使用規則,這組屬性或使用規則對執行時相關的對象集合是通用的.能夠添加的上下文屬性包括有關同步,線程式親緣性的事務處理的策略.簡言之,上下文把類似的對象組合在一起.這種策略將使用SynchronizationArrribute類對ContextBoundObject對象進行簡單的自動同步,駐留在上下文中,符合上下文規則的對象稱爲上下文對象稱爲上下文綁定對象. .NET把同步鎖和對象自動關聯起來.在每種方法調用前鎖定對象,方法返回後釋放對象(讓其它等待着的線程訪問).由於線程同步和併發管理是開發人員遇到的最困難的任務.因此這種方法極大的提高了效率.
    SynochronizationAttribute 類對缺少手工處理同步經難的程序員來說是很有用的.因爲它包含了實例變量,實例方法和應用這個屬性的類的實例字段.然而,它不是處理靜態字段和方法同步.如果必須同步代碼塊,它也不起作用.同步整個對象是對輕鬆使用必須付出的代碼.在使用SynchronizationAttribute編程時,SynchronizationAttribute非常方便.因爲一個上下文(例如事務處理)中的對象由 COM+ 運行庫組合在一起.我們可以使用下面的代碼來使用一個類具有安全性:
    [SynchronizationAttribute(SynchronizationOption.Required)]
    public class className
    {
            //class code here
    }
   
    SynchronizationAttribute類有兩個構造函數.一個不帶參數(默認SynchronizationOption.Required),另一個構造函數包含一個SynchronizationOption 枚舉.這個枚舉的值如下所示:

Disabled            :忽略對象的同步請求,即對象不是線程安全的.
NotSupported    :創建組件時沒有控制同步,即論調用者牌什麼狀態,對象都不能參與任何同步操作.
Required            :確保所創建的所有的對象都已同步.
RequiredNew    :無論調用者是什麼,組件總是參與新的同步.
Supported           :使用這個選項的對象只有存在(取決於調用者)時,才能參與同步操作.

2 同步代碼區
    第二種同步策略是同步代碼區,這些特定的代碼區是方法中重要的代碼段.它們可以改變對象的狀態,或者更新另一個資源(如數據庫,文件等).先看Monitor類.
    Monitor類用於同步代碼區,其方式是使用Monitor.Enter()方法獲得一個鎖.然後,使用Monitor.Exit()方法釋放該鎖.鎖的概念通常用於解釋Monitor類.一個線程獲得鎖,其它線程就要等到該鎖釋放後才能使用.一旦在代碼區上獲取了一個鎖,就可以在Monitor.Enter()和Monitor.Exit()程序塊內使用如下方法:
    Wait()--此方法用於釋放對象上的鎖.並暫停當前的線程.直到它重新獲得鎖.
    Pulse()-此方法用於通知正在隊列中等待的線程,對象的狀態已經改變了.
    PulseAll():此方法用於通知所有正在隊列中等待的線程,對象的狀態已經有了改變.

    Enter()方法和Exit()方法
    Monitor方法是靜態方法.能夠被Monitor類自身調用,而不是由該類的實例調用.在.NET Framework中,每個對象都有一個與之相關的鎖,可以獲取和釋放該鎖,這樣,在任一時刻僅有一個線程可以訪問對象的實例變量和方法.與之類似,.NET Framework中的每個對象也提供一種允許它處於等待狀態的機制.正如鎖的機制. 設計這種機制的主要原因是幫助線程間的通信.如果一個線程進入對象的重要代碼段,並需要一定的條件才能存在,而另一個線程可以在該代碼段中創建該條件,此時就需要這種機制.
    訣竅在於,在任一時刻,重要的代碼段只允許一個線程訪問,當第一個線程進入代碼段後,其它的線程就不能再進入了,如果第線程A從數據庫中獲取了一些數據,在線程A接收到所有的數據,並處理這些數據的過程中,另一個線程B必須調用Wait()方法.等待線程A通知它何時才能接收數據.在接收到數據時,線程A就調用Pulse()方法,通知線程B,讓線程B處理數據.這可以通過"等待和發出脈衝"機制來實現.第一個線程進入重要代碼段,執行Wait() 方法. Wait() 方法先釋放鎖, 再使第一個線程進入等待狀態.現在第二個線程進入代碼段.修改必須的條件.然後調用Pulse() 方法來通知等待的線程.當條件滿足後,就可以繼續執行.接着第一個線程先重新獲得鎖,再從Monitor.Wait() 方法中返回,並從調用Monitor.Wait()的地方繼續執行.任何兩個線程都不能同時進入 Enter()  方法.
    下面是一個有關Monitor的例子:
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;

namespace ConsoleApplication1
{
    class Program
    {
        private int result = 0;
        public Program()
        {
        }
        public void NonCriticalSection()
        {
            Console.WriteLine("Enter thread " + Thread.CurrentThread.GetHashCode());
            for (int i = 0; i <= 15; i++)
            {
                Console.WriteLine("Result = " + result++ + " ThreadID=" + Thread.CurrentThread.GetHashCode());
                Thread.Sleep(2500);
            }
            Console.WriteLine("Exiting Thread" + Thread.CurrentThread.GetHashCode());
        }
        public void CriticalSection()
        {
            Monitor.Enter(this);
            Console.WriteLine("Entered Thread " + Thread.CurrentThread.GetHashCode());
            for (int i = 0; i <= 15; i++)
            {
                Console.WriteLine("Result = " + result++ + " ThreadID=" + Thread.CurrentThread.GetHashCode());
                Thread.Sleep(1500);
            }
            Console.WriteLine("Exiting Thread" + Thread.CurrentThread.GetHashCode());
            Monitor.Exit(this);
        }
        public static void Main()
        {
            Program p = new Program();
            int i = Console.Read();
            if (i % 2 == 0)
            {
                Thread t1 = new Thread(new ThreadStart(p.NonCriticalSection));
                t1.Start();
                Thread t2 = new Thread(new ThreadStart(p.NonCriticalSection));
                t2.Start();
            }
            else
            {
                Thread t1 = new Thread(new ThreadStart(p.CriticalSection));
                t1.Start();
                Thread t2 = new Thread(new ThreadStart(p.CriticalSection));
                t2.Start();
            }
        }
    }
}

在上面的例子中,重要代碼段定義爲 Monitor.Enter(this) 和 Monitor.Exit(this)之間的代碼塊.參數this指定應將鎖保存在當前對象上,把哪個對象作爲參數傳遞給Enter()方法況是不好確定.在需要鎖定對象,不上其他線程訪問它時,應把指針this作爲參數傳遞.

Wait()和Pulse()機制
    Wait()和Pulse()機制用於線程式間的交互.當在一個對象上執行Wait()時,正在訪問該對象的線程就會進入等待狀態,直到它得到一個喚醒信號.Pulse()和PulseAll()用於給等待線程發送信號.下面列出的上一個Wait()和Pulse()方法如何工作的例子:
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Threading;

namespace ConsoleApplication1
{
    class LockMe
    {
        public LockMe()
        {
        }
    }
    class Monitor1
    {
        public LockMe _lm;
        public Monitor1(LockMe lm)
        {
            this._lm = lm;
        }

        public void CriticalSection()
        {
            Monitor.Enter(this._lm);
            Console.WriteLine("Enter M1");
            for(int i= 0 ;i < 10; i++)
            {
                Thread.Sleep(3000);
                Console.WriteLine("M1 Wait");
                Monitor.Wait(this._lm);

                Console.WriteLine("M1 WorkUP");
                Monitor.Pulse(this._lm);
               
            }
            Monitor.Exit(this._lm);
        }
    }
    class Monitor2
    {
        private LockMe _lm;
        public Monitor2(LockMe lm)
        {
            this._lm = lm;
        }
        public void CriticalSection()
        {
            Monitor.Enter(this._lm);
            Console.WriteLine("Enter M2");
            for (int i = 0; i < 10; i++)
            {
                Thread.Sleep(3000);
                Console.WriteLine("M2 WorkUp");
                Monitor.Pulse(this._lm);
                Console.WriteLine("M2 Wait");
                Monitor.Wait(this._lm);
            }
            Monitor.Exit(this._lm);
        }
    }
    class Program
    {
        public static void Main()
        {
            LockMe l = new LockMe();
            Monitor1 m1 = new Monitor1(l);
            Monitor2 m2 = new Monitor2(l);

            Thread t1 = new Thread(new ThreadStart(m1.CriticalSection));
            t1.Start();

            Thread t2 = new Thread(new ThreadStart(m2.CriticalSection));
            t2.Start();
        }
    }
}
注意:當線程執行Monitor.Wait()方法時,它會臨時釋放LockMe對象上的鎖,以便其它線程可以訪問它.線程 t1 進入等待狀態後, 線程t2就可以自由訪問 LockMe 對象.即使LockMe對象是一個獨立的對象,這兩個線程也均指向同一個對象的引用. 線程t2獲得LockMe對象上的鎖後,一進入For循環,就給LockMe對象上等待的線程發送一個運行進通知.之後便進行等待狀態.然後t1 醒來,獲得LockMe對象上的鎖,接着 t1 執行一些操作,並給  LockMe對象上等待的線程發送一個運行時通知.這個循環一直持續到For循環結束.
注意每一個 Enter() 方法都要伴隨一個 Exit()方法,否則程序就會進入列循環.
    Enter將一個對象作爲其參數,如果該對象參數爲 null, 方法變量或者值類型對象如整型值,系統將會拋出一個異常.

TryEnter()方法
   
Moniter類的TryEnter()方法非常類似於Enter()方法,它試圖獲得對象的獨佔鎖,不過它不會像Enter()方法那樣暫停.如果線程成功進入,則TryEnter()方法返回True.
   
TryEnter()方法有三個重載,其中兩個都帶超時參數,表示等待鎖定的時間.如:
    Monitor.TryEnter(this,1000);
    ......
    在可能發生競爭,但不希望線程睡眠某個示指定的時間,就可以使用TryEnter().
Lock
    Lock關鍵字可以用作monitor方法的一個替換用法,下面的兩部分代碼是等價的:
    Monitor.Enter(X);
    .....
    Monitor.Exit(X);

    lock(this)
    {
       .....
    }

ReaderWriterLock類
    ReaderWriterLock定義了實現單寫程序和多讀程序語義的鎖.這個類主要用於文件操作.即多個線程可以讀取文件,但只能用一個線程來更新文件.ReaderWriterLock類中4個主要的方法是:
     AcquireReaderLock():該重載方法獲得一個讀程序鎖,超時值使用整數或TimeSpan.超時是可用於檢測死鎖的好工具.
    AcquireWriterLock():該重載方法獲得一個寫程序鎖.超時值使用整數或TimeSpan.
    ReleaseReaderLock():釋放讀程序鎖.
    ReleaseWriterLock():釋放寫程序鎖.

    使用ReaderWriterLock類時,任意數量的線程都可以同時安全的讀取數據.只有當線程進行更新時,數據才被鎖定.只有在沒有佔用鎖的寫程序線程時,讀程序線程才能獲得鎖.只有沒有佔用鎖的讀程序或寫程序線程時,寫程序才能獲得鎖.
   

3手控同步
    DotNet Framework提供了一套經典的技術允許程序員使用類似於Win32線程的低級線程API創建和管理多線程應用程序.下表列出了System.Threading命名空間中的一些可以用於手控同步和類.
   
    AutoResetEvent:
       AutoResetEvent類可以使線程處於等待狀態.直到通過調用Set()方法某事件將它置於信號狀態爲止.有信號狀態是指沒有線程在等待.在釋放一個等待線程後.系統自動將AutoResetEvent類生置爲無信號狀態.如果沒正在等待的線程,事件對象的狀態保持警惕爲有信號狀態.指定ManualRest參數爲False時,AutoResetEvent類對應於Win32的CreateEvent調用.
   
    ManualResetEvent
       ManualResetEvent類也用來使線程式處於等待狀態.直到通過調用Set()方法某事件將它置於有信號狀態.ManualResetEvent對象的狀態會一直保持有信號,直到Reset()方法將它設置爲無信號狀態.指定 bManualReset 參數爲True時, ManualResetEvent對應於Win32的CreateEvent調用.

    Mutex
       Mutex鎖提供了跨進程和跨線程同步.如果沒有線程佔用Mutex鎖,Mutex的狀態就軒爲有信號狀態.Mutex並不上具有Monitor類的所有等待和發出脈衝功能.但可以創建在進程之間使用的命名互斥(使用重載構造函數),使用Mutex 類優於使用 Monitor 類,因爲Mutex類可以跨進程使用,而Monitor類不行.

    Interlocked
       Interlocked類爲在多個線程之間共享的原子的,非暫停整體更新提供了方法.如果變量位於共享的內在中,不同進程的線程就可以使用這種機制.

 ManualResetEvent類
       ManulaResetEvent類對象只能擁有兩種狀態之一:有信號(True)和無信號(False),ManualResetEvent類繼承於WaitHandler類,其構造函數的參數可以確定對象的初始狀態,Set()和Reset()方法返回一個布爾值,表示是否進行了成功的修改.
    下面的代碼首先創建一個對象 mansig , 並賦予false值,方法WaitOne()將一直等待 mansig 變爲 true 或超時爲止. 由於時間在等待中過去.且 mansig 的值沒有被設置障爲 true,所以程序就停止暫停並返回 false值.
       public static void  Main(string [] args)
        {
             ManualResetEvent mansig;
             mansig = new ManualResetEvent(false);
             Console.WriteLine("ManualResetEvent Before WaitOne");
             bool b = mansig.WaitOne(1000,false);
             Console.WriteLine("ManualResetEvent After WaitOne" + b);
          }
     上面的代碼輸出如下:
          ManualResetEvent Before Waitone
          ManulaResetEvent After WaitOne false
       布爾值 false 把ManualResetEvent對象的初始狀態設置爲無信號,接着調用基類的 WaitHandle的 WaitOne() 方法,WaitOne()方法有兩個參數,第一個參數是線程在WaitOne()方法中等待的毫秒數,因此在線程退出前等待一秒,第二個參數是exitContext , 如果已以在上下文的同步域中,想退出同步上下文,或者要重新獲得同步上下文,就把該參數設置爲 true;程序在WaitOne()方法中暫停一秒,然後因爲超時而退出.ManulaResetEvent的狀態仍然是 false,因而WaitOne()返回的布爾值是 false. 如果在創建 ManualResetEvent時就把它的狀態設定爲有信號(treu)時,如下代碼:
       ManualResetEvent mansig;
       mansig = new ManualResetEvent(true):
       Console.WriteLine("ManualResetEvent Before WaitOne");
       bool b = mansig.WaitOne(1000,false);
       Console.WriteLine("ManualResetEvent After WaitOne " + b);
    輸出結果如下所示:
       ManualResetEvent Before WaitOne
       ManualResetEvent After WaitOne true

       將ManualResetEvent的初始狀態修改爲有信號的,即使指定超時值爲1000毫秒,線程在WaitOne()中也不會等待.當ManualResetEvent的狀態爲無信號狀態時,線程將等待狀態變爲有信號狀態. 但1000毫秒後線程超時.狀態 已經是有信號的.必須調用ManualResetEvent 的Reset()方法.爲了把狀態修改爲有信號的,必須調用Set()方法.
       下面的代碼演示瞭如何使用 Set() 方法和 Reset()方法.
       static void Main()
       {
             ManualResetEvent mre = new ManualResetEvent(true);
             mre = new ManualResetEvent(1000,true);
             bool state = mre.WaitOne(1000,true);
             Console.WriteLine("ManualResetEvent After WaitOne" + state);
           
             //change the state non-signaled
             mre.Reset();
             state = mre.WaitOne(5000,true);
             Console.WriteLine("ManualResetEvent After secodn WaitOne" + state);    
        }
       程序輸出:
       ManualResetEvent Ater first WaitOne  true
       ManualResetEvent After Second WaitOne false
       ManualResetEvent對象的構造函數在將其初始化時設置爲有信號的(true),結果,線程不在第一個WaitOne()方法中等待,並返回 true 值,接着將ManualResetEvent對象的狀態重新設置爲無信號的(false),於是線程在超時之間等待5秒鐘.
       ManualResetEvent對象的WaitAll() 方法將等待所有的事件對象都變成 true 或者有信號時,否則它將保持不變直到超時.WaitAny()方法等待任一事件對象變成true或有信號.
   
AutoResetEvent類
      
AutoResetEvent類的工作方式類似於ManualResetEvent類,它等待時間超時或者事件變成有信號的狀態.接着將此事件通知等待線程.ManualResetEvent和AutoResetEvent的一個重要區別就是AutoResetEvent在WaitOne()方法中改變狀態.下面的代碼展示了AutoResetEvent類的用法:
       static void Main()
       {
          AutoResetEvent 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);
       }

    靜態變量,方法和同步
       靜態變量和方法與同步鎖中的實例 變量和方法有不同的作用.靜態變量是類變量,而屬於一個對象的變量是對象或者實例變量.靜態變量只能有一個實例,靜態方法由同一類的多個對象共享.同一類的每個對象都有自己的一組實例變量和方法.如果同步一個靜態變量或者靜態方法,鎖就要應用於整個類上,其結果是,其它對象不能使用此類的靜態變量.
ThreadStaticAttribute 類
       ThreadStaticAttribute類用於在靜態變量上爲每個執行它的線程創建一個單獨的變量,而不是在線程之間共享(默認行爲)靜態變量.這意味着,帶有ThreadStaticAttribute的靜態變量不會在訪問變量的不同線程之間共享.每個訪問變量的線程都有該變量的一個副本.如果一個線程修改了變量,另一個訪問變量的線程就不能看到這些變化.這種行爲是有背於靜態變量的默認行爲的.總之,ThreadStaticAttribute提供了最好的方法(靜態和實例).如下代碼:
    class Program
    
{
        
static void Main(string[] args)
        
{
            StaticThread ts 
= new StaticThread();
            Thread t1 
= new Thread(new ThreadStart(ts.Run));
            Thread t2 
= new Thread(new ThreadStart(ts.Run));
            t1.Start();
            t2.Start();

            Thread.Sleep(
1000000);
        }

    }

    
class StaticThread
    
{
        [System.ThreadStatic]
        
public static int x = 1;
        
public static int y = 1;
        
public void Run()
        
{
            
for (int i = 0; i <= 10; i++)
            
{
                Thread t2 
= Thread.CurrentThread;
                x
++;
                y
++;                Console.WriteLine("i=" + i + "ThreadID=" + t2.GetHashCode() + "x(static attribute)= " + x + " y = " + y);
                Thread.Sleep(
1000);
            }

        }


    }
        通過運行上面的代碼可知:靜態變量是類量,它的值在類的多個對象中保持不變,ThreadStaticAttribute允許每個訪問靜態變量的線程擁有自己的副本,上面的代碼中,變量x 使 ThreadStaticAttribute應用於它,結果,線程t1和t2將分別擁有靜態變量x的一個副本,並且線程t1對變量x的修改對線程t2來說是不可見的.另一方面,線程t1對變量y的修改對線程t2來說是可見的.
       帶有ThreadStaticAttribute的靜態變量和實例變量之間的區別是:靜態變量不需求對象訪問它,但如果沒有創建對象的實例就試圖訪問實例變量,剛會拋出異常.


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