[轉]C#學習筆記15——C#多線程編程

1、基本概念
進程:當一個程序開始運行時,它就是一個進程,進程包括運行中的程序和程序所使用到的內存和系統資源。而一個進程又是由多個線程所組成的。
線程:線程是程序中的一個執行流,每一個線程都有本身的專有寄存器(棧指針、程序計數器等),但代碼區是共享的,即不一樣的線程能夠執行一樣的函數。
多線程:多線程是指程序中包含多個執行流,即在一個程序中能夠同時運行多個不一樣的線程來執行不一樣的任務,也就是說容許單個程序建立多個並行執行的線程來完成各自的任務。
靜態屬性:這個類全部對象所公有的屬性,無論你建立了多少個這個類的實例,可是類的靜態屬性在內存中只有一個。php

2、多線程的優劣
優勢:能夠提升CPU的利用率。在多線程程序中,一個線程必須等待的時候,CPU能夠運行其它的線程而不是等待,這樣就大大提升了程序的效率。
缺點:線程也是程序,因此線程須要佔用內存,線程越多佔用內存也越多;
多線程須要協調和管理,因此須要CPU時間跟蹤線程;
線程之間對共享資源的訪問會相互影響,必須解決競用共享資源的問題;
線程太多會致使控制太複雜,最終可能形成不少Bug;web

3、控制線程的類和方法
類:usingSystem.Threading; Thread類數據庫

Thread類經常使用屬性:
Name:線程的名稱
CurrentThread:當前線程
Priority:線程優先級。在C#應用程序中,用戶能夠設定5個不一樣的優先級,由高到低分別是 Highest,AboveNormal,Normal,BelowNormal,Lowest,在建立線程時若是不指定優先級,那麼系統默認爲ThreadPriority.Normal。
ThreadState:當前線程狀態。這個屬性表明了線程運行時狀態,在不一樣的狀況下有不一樣的值,咱們有時候能夠經過對該值的判斷來設計程序流程。C# ThreadState屬性的取值以下:
◆Aborted:線程已中止;
◆AbortRequested:線程的Thread.Abort()方法已被調用,可是線程還未中止;
◆Background:線程在後臺執行,與屬性Thread.IsBackground有關;
◆Running:線程正在正常運行;
◆Stopped:線程已經被中止;
◆StopRequested:線程正在被要求中止;
◆Suspended:線程已經被掛起(此狀態下,能夠經過調用Resume()方法從新運行);
◆SuspendRequested:線程正在要求被掛起,可是將來得及響應;
◆Unstarted:未調用Thread.Start()開始線程的運行;
◆WaitSleepJoin:線程由於調用了Wait(),Sleep()或Join()等方法處於封鎖狀態;編程

上面提到了Background狀態表示該線程在後臺運行,那麼後臺運行的線程有什麼特別的地方呢?其實後臺線程跟前臺線程只有一個區別,那就是後臺線程不妨礙程序的終止。一旦一個進程全部的前臺線程都終止後,CLR(通用語言運行環境)將經過調用任意一個存活中的後臺進程的Abort()方法來完全終止進程。數組


Thread類經常使用方法:
Start():開始執行線程
Suspend():掛起線程
Resume():恢復被掛起的線程
Sleep():暫停線程
Interrupt():中斷線程
Join():阻塞調用線程,直到某個線程終止時爲止;一個阻塞調用,直到線程的確是終止了才返回。
Abort():停止線程緩存


建立一個線程通常包含三個步驟:
一、建立入口函數
二、建立入口委託
三、建立線程安全

 

4、線程同步數據結構

多個線程的同步技術:多線程

lock語句app

Interlocaked類

Monitor類

等待句柄

Mutex類

Semaphore類

Event類

ReaderWriterLockSlim

lock語句、Interlocaked類、Monitor類可用於進程內部的同步。Mutex類、Semaphore類、Event類、ReaderWriterLockSlim類提供了多個進程中的線程同步。

.NET Framework的CLR提供了三種方法來完成對共享資源,諸如全局變量域,特定的代碼段,靜態的和實例化的方法和域。

  (1) 代碼域同步:使用Monitor類能夠同步靜態/實例化的方法的所有代碼或者部分代碼段。不支持靜態域的同步。在實例化的方法中,this指針用於同步;而在靜態的方法中,類用於同步,這在後面會講到。

  (2) 手工同步:使用不一樣的同步類(諸如WaitHandle, Mutex, ReaderWriterLock, ManualResetEvent,AutoResetEvent 和Interlocked等)建立本身的同步機制。這種同步方式要求你本身手動的爲不一樣的域和方法同步,這種同步方式也能夠用於進程間的同步和對共享資源的等待而形成的死鎖解除。

  (3) 上下文同步:使用SynchronizationAttribute爲ContextBoundObject對象建立簡單的,自動的同步。這種同步方式僅用於實例化的方法和域的同步。全部在同一個上下文域的對象共享同一個鎖。
Monitor Class

  在給定的時間和指定的代碼段只能被一個線程訪問,Monitor 類很是適合於這種狀況的線程同步。這個類中的方法都是靜態的,因此不須要實例化這個類。下面一些靜態的方法提供了一種機制用來同步對象的訪問從而避免死鎖和維護數據的一致性。

  Monitor.Enter方法:在指定對象上獲取排他鎖。

  Monitor.TryEnter方法:試圖獲取指定對象的排他鎖。

  Monitor.Exit方法:釋放指定對象上的排他鎖。

  Monitor.Wait方法:釋放對象上的鎖並阻塞當前線程,直到它從新獲取該鎖。

  Monitor.Pulse方法:通知等待隊列中的線程鎖定對象狀態的更改。

  Monitor.PulseAll方法:通知全部的等待線程對象狀態的更改。

  經過對指定對象的加鎖和解鎖能夠同步代碼段的訪問。Monitor.Enter, Monitor.TryEnter 和 Monitor.Exit用來對指定對象的加鎖和解鎖。一旦獲取(調用了Monitor.Enter)指定對象(代碼段)的鎖,其餘的線程都不能獲取該鎖。舉個例子來講吧,線程X得到了一個對象鎖,這個對象鎖能夠釋放的(調用Monitor.Exit(object) or Monitor.Wait)。當這個對象鎖被釋放後,Monitor.Pulse方法和 Monitor.PulseAll方法通知就緒隊列的下一個線程進行和其餘全部就緒隊列的線程將有機會獲取排他鎖。線程X釋放了鎖而線程Y得到了鎖,同時調用Monitor.Wait的線程X進入等待隊列。當從當前鎖定對象的線程(線程Y)受到了Pulse或PulseAll,等待隊列的線程就進入就緒隊列。線程X從新獲得對象鎖時,Monitor.Wait才返回。若是擁有鎖的線程(線程Y)不調用Pulse或PulseAll,方法可能被不肯定的鎖定。Pulse, PulseAll andWait必須是被同步的代碼段被調用。對每個同步的對象,你須要有當前擁有鎖的線程的指針,就緒隊列和等待隊列(包含須要被通知鎖定對象的狀態變化的線程)的指針。

  你也許會問,當兩個線程同時調用Monitor.Enter會發生什麼事情?不管這兩個線程地調用Monitor.Enter是多麼地接近,實際上確定有一個在前,一個在後,所以永遠只會有一個得到對象鎖。既然Monitor.Enter是原子操做,那麼CPU是不可能偏好一個線程而不喜歡另一個線程的。爲了獲取更好的性能,你應該延遲後一個線程的獲取鎖調用和當即釋放前一個線程的對象鎖。對於private和internal的對象,加鎖是可行的,可是對於external對象有可能致使死鎖,由於不相關的代碼可能由於不一樣的目的而對同一個對象加鎖。

  若是你要對一段代碼加鎖,最好的是在try語句裏面加入設置鎖的語句,而將Monitor.Exit放在finally語句裏面。對於整個代碼段的加鎖,你可使用MethodImplAttribute(在System.Runtime.CompilerServices命名空間)類在其構造器中設置同步值。這是一種能夠替代的方法,當加鎖的方法返回時,鎖也就被釋放了。若是須要要很快釋放鎖,你可使用Monitor類和C# lock的聲明代替上述的方法。

  讓咱們來看一段使用Monitor類的代碼:
public void some_method()
{

int a=100;

int b=0;

Monitor.Enter(this);

//say we do something here.

int c=a/b;

Monitor.Exit(this);

}

  上面的代碼運行會產生問題。當代碼運行到int c=a/b; 的時候,會拋出一個異常,Monitor.Exit將不會返回。所以這段程序將掛起,其餘的線程也將得不到鎖。有兩種方法能夠解決上面的問題。第一個方法是:將代碼放入try…finally內,在finally調用Monitor.Exit,這樣的話最後必定會釋放鎖。第二種方法是:利用C#的lock()方法。調用這個方法和調用Monitoy.Enter的做用效果是同樣的。可是這種方法一旦代碼執行超出範圍,釋放鎖將不會自動的發生。見下面的代碼:
public void some_method()
{

int a=100;

int b=0;

lock(this);

//say we do something here.

int c=a/b;

}

  C# lock申明提供了與Monitoy.Enter和Monitoy.Exit一樣的功能,這種方法用在你的代碼段不能被其餘獨立的線程中斷的狀況。
WaitHandle Class

  WaitHandle類做爲基類來使用的,它容許多個等待操做。這個類封裝了win32的同步處理方法。WaitHandle對象通知其餘的線程它須要對資源排他性的訪問,其餘的線程必須等待,直到WaitHandle再也不使用資源和等待句柄沒有被使用。下面是從它繼承來的幾個類:

  Mutex 類:同步基元也可用於進程間同步。

  AutoResetEvent:通知一個或多個正在等待的線程已發生事件。沒法繼承此類。

  ManualResetEvent:當通知一個或多個正在等待的線程事件已發生時出現。沒法繼承此類。

  這些類定義了一些信號機制使得對資源排他性訪問的佔有和釋放。他們有兩種狀態:signaled 和 nonsignaled。Signaled狀態的等待句柄不屬於任何線程,除非是nonsignaled狀態。擁有等待句柄的線程再也不使用等待句柄時用set方法,其餘的線程能夠調用Reset方法來改變狀態或者任意一個WaitHandle方法要求擁有等待句柄,這些方法見下面:

  WaitAll:等待指定數組中的全部元素收到信號。

  WaitAny:等待指定數組中的任一元素收到信號。

  WaitOne:當在派生類中重寫時,阻塞當前線程,直到當前的 WaitHandle 收到信號。

  這些wait方法阻塞線程直到一個或者更多的同步對象收到信號。

  WaitHandle對象封裝等待對共享資源的獨佔訪問權的操做系統特定的對象不管是收管代碼仍是非受管代碼均可以使用。可是它沒有Monitor使用輕便,Monitor是徹底的受管代碼並且對操做系統資源的使用很是有效率。


Mutex Class

  Mutex是另一種完成線程間和跨進程同步的方法,它同時也提供進程間的同步。它容許一個線程獨佔共享資源的同時阻止其餘線程和進程的訪問。Mutex的名字就很好的說明了它的全部者對資源的排他性的佔有。一旦一個線程擁有了Mutex,想獲得Mutex的其餘線程都將掛起直到佔有線程釋放它。Mutex.ReleaseMutex方法用於釋放Mutex,一個線程能夠屢次調用wait方法來請求同一個Mutex,可是在釋放Mutex的時候必須調用一樣次數的Mutex.ReleaseMutex。若是沒有線程佔有Mutex,那麼Mutex的狀態就變爲signaled,不然爲nosignaled。一旦Mutex的狀態變爲signaled,等待隊列的下一個線程將會獲得Mutex。Mutex類對應與win32的CreateMutex,建立Mutex對象的方法很是簡單,經常使用的有下面幾種方法:

  一個線程能夠經過調用WaitHandle.WaitOne或WaitHandle.WaitAny 或 WaitHandle.WaitAll獲得Mutex的擁有權。若是Mutex不屬於任何線程,上述調用將使得線程擁有Mutex,並且WaitOne會當即返回。可是若是有其餘的線程擁有Mutex,WaitOne將陷入無限期的等待直到獲取Mutex。你能夠在WaitOne方法中指定參數即等待的時間而避免無限期的等待Mutex。調用Close做用於Mutex將釋放擁有。一旦Mutex被建立,你能夠經過GetHandle方法得到Mutex的句柄而給WaitHandle.WaitAny 或 WaitHandle.WaitAll 方法使用。

  下面是一個示例:
public void some_method()
{

int a=100;

int b=20;

Mutex firstMutex = new Mutex(false);

FirstMutex.WaitOne();

//some kind of processing can be done here.

Int x=a/b;

FirstMutex.Close();

}

  在上面的例子中,線程建立了Mutex,可是開始並無申明擁有它,經過調用WaitOne方法擁有Mutex。

Synchronization Events

  同步時間是一些等待句柄用來通知其餘的線程發生了什麼事情和資源是可用的。他們有兩個狀態:signaled and nonsignaled。AutoResetEvent 和 ManualResetEvent就是這種同步事件。


AutoResetEvent Class

  這個類能夠通知一個或多個線程發生事件。當一個等待線程獲得釋放時,它將狀態轉換爲signaled。用set方法使它的實例狀態變爲signaled。可是一旦等待的線程被通知時間變爲signaled,它的轉檯將自動的變爲nonsignaled。若是沒有線程偵聽事件,轉檯將保持爲signaled。此類不能被繼承。


ManualResetEvent Class

  這個類也用來通知一個或多個線程事件發生了。它的狀態能夠手動的被設置和重置。手動重置時間將保持signaled狀態直到ManualResetEvent.Reset設置其狀態爲nonsignaled,或保持狀態爲nonsignaled直到ManualResetEvent.Set設置其狀態爲signaled。這個類不能被繼承。


Interlocked Class

  它提供了在線程之間共享的變量訪問的同步,它的操做時原子操做,且被線程共享.你能夠經過Interlocked.Increment或Interlocked.Decrement來增長或減小共享變量.它的有點在因而原子操做,也就是說這些方法能夠代一個整型的參數增量而且返回新的值,全部的操做就是一步.你也可使用它來指定變量的值或者檢查兩個變量是否相等,若是相等,將用指定的值代替其中一個變量的值.


ReaderWriterLock class

  它定義了一種鎖,提供惟一寫/多讀的機制,使得讀寫的同步.任意數目的線程均可以讀數據,數據鎖在有線程更新數據時將是須要的.讀的線程能夠獲取鎖,當且僅當這裏沒有寫的線程.當沒有讀線程和其餘的寫線程時,寫線程能夠獲得鎖.所以,一旦writer-lock被請求,全部的讀線程將不能讀取數據直到寫線程訪問完畢.它支持暫停而避免死鎖.它也支持嵌套的讀/寫鎖.支持嵌套的讀鎖的方法是ReaderWriterLock.AcquireReaderLock,若是一個線程有寫鎖則該線程將暫停;

支持嵌套的寫鎖的方法是ReaderWriterLock.AcquireWriterLock,若是一個線程有讀鎖則該線程暫停.若是有讀鎖將容易卻是死鎖.安全的辦法是使用ReaderWriterLock.UpgradeToWriterLock方法,這將使讀者升級到寫者.你能夠用ReaderWriterLock.DowngradeFromWriterLock方法使寫者降級爲讀者.調用ReaderWriterLock.ReleaseLock將釋放鎖, ReaderWriterLock.RestoreLock將從新裝載鎖的狀態到調用ReaderWriterLock.ReleaseLock之前。

 

5、線程池和定時器——多線程的自動管理

在多線程的程序中,常常會出現兩種狀況。一種狀況下,應用程序中的線程把大部分的時間花費在等待狀態,等待某個事件發生,而後才能給予響應;而另一種狀況則是線程日常都處於休眠狀態,只是週期性地被喚醒。在.net framework裏邊,咱們使用ThreadPool來對付第一種狀況,使用Timer來對付第二種狀況。

  ThreadPool類提供一個由系統維護的線程池——能夠看做一個線程的容器,該容器須要Windows 2000以上版本的系統支持,由於其中某些方法調用了只有高版本的Windows纔有的API函數。你可使用ThreadPool.QueueUserWorkItem()方法將線程安放在線程池裏,該方法的原型以下:

  //將一個線程放進線程池,該線程的Start()方法將調用WaitCallback代理對象表明的函數
  publicstatic bool QueueUserWorkItem(WaitCallback);
  //重載的方法以下,參數object將傳遞給WaitCallback所表明的方法
  publicstatic bool QueueUserWorkItem(WaitCallback, object);


  要注意的是,ThreadPool類也是一個靜態類,你不能也沒必要要生成它的對象,並且一旦使用該方法在線程池中添加了一個項目,那麼該項目將是沒有辦法取消的。在這裏你無需本身創建線程,只需把你要作的工做寫成函數,而後做爲參數傳遞給ThreadPool.QueueUserWorkItem()方法就好了,傳遞的方法就是依靠WaitCallback代理對象,而線程的創建、管理、運行等等工做都是由系統自動完成的,你無須考慮那些複雜的細節問題,線程池的優勢也就在這裏體現出來了,就好像你是公司老闆——只須要安排工做,而沒必要親自動手。
下面的例程演示了ThreadPool的用法。首先程序建立了一個ManualResetEvent對象,該對象就像一個信號燈,能夠利用它的信號來通知其它線程,本例中當線程池中全部線程工做都完成之後,ManualResetEvent的對象將被設置爲有信號,從而通知主線程繼續運行。它有幾個重要的方法:Reset(),Set(),WaitOne()。初始化該對象時,用戶能夠指定其默認的狀態(有信號/無信號),在初始化之後,該對象將保持原來的狀態不變直到它的Reset()或者Set()方法被調用,Reset()方法將其設置爲無信號狀態,Set()方法將其設置爲有信號狀態。WaitOne()方法使當前線程掛起直到ManualResetEvent對象處於有信號狀態,此時該線程將被激活。而後,程序將向線程池中添加工做項,這些以函數形式提供的工做項被系統用來初始化自動創建的線程。當全部的線程都運行完了之後,ManualResetEvent.Set()方法被調用,由於調用了ManualResetEvent.WaitOne()方法而處在等待狀態的主線程將接收到這個信號,因而它接着往下執行,完成後邊的工做。

  usingSystem;
  usingSystem.Collections;
  usingSystem.Threading;

  //這是用來保存信息的數據結構,將做爲參數被傳遞
  public classSomeState
  {
  public intCookie;
  publicSomeState(int iCookie)
  {
    Cookie =iCookie;
  }
  }

  public classAlpha
  {
  publicHashtable HashCount;
  publicManualResetEvent eventX;
  publicstatic int iCount = 0;
  publicstatic int iMaxCount = 0;
  publicAlpha(int MaxCount) 
  {
    HashCount= new Hashtable(MaxCount);
    iMaxCount= MaxCount;
  }

  file://線程池裏的線程將調用Beta()方法
  public voidBeta(Object state)
  {
    //輸出當前線程的hash編碼值和Cookie的值
    Console.WriteLine("{0} {1} :", Thread.CurrentThread.GetHashCode(),
    ((SomeState)state).Cookie);
    Console.WriteLine("HashCount.Count=={0},Thread.CurrentThread.GetHashCode()=={1}", HashCount.Count,Thread.CurrentThread.GetHashCode());
    lock(HashCount) 
    {
    file://若是當前的Hash表中沒有當前線程的Hash值,則添加之
    if(!HashCount.ContainsKey(Thread.CurrentThread.GetHashCode()))
      HashCount.Add(Thread.CurrentThread.GetHashCode(), 0);
    HashCount[Thread.CurrentThread.GetHashCode()]= 
((int)HashCount[Thread.CurrentThread.GetHashCode()])+1;
    }

    int iX =2000;
    Thread.Sleep(iX);
    //Interlocked.Increment()操做是一個原子操做,具體請看下面說明
    Interlocked.Increment(refiCount);
    if (iCount== iMaxCount)
    {
    Console.WriteLine();
    Console.WriteLine("SettingeventX ");
    eventX.Set();
    }
  }
  }

  public classSimplePool
  {
  publicstatic int Main(string[] args)
  {
    Console.WriteLine("ThreadPool Sample:");
    bool W2K =false;
    intMaxCount = 10;//容許線程池中運行最多10個線程
    //新建ManualResetEvent對象而且初始化爲無信號狀態
    ManualResetEventeventX = new ManualResetEvent(false);
    Console.WriteLine("Queuing{0} items to Thread Pool", MaxCount);
    AlphaoAlpha = new Alpha(MaxCount); file://建立工做項
    //注意初始化oAlpha對象的eventX屬性
    oAlpha.eventX= eventX;
    Console.WriteLine("Queueto Thread Pool 0");
    try
    {
    file://將工做項裝入線程池 
    file://這裏要用到Windows 2000以上版本纔有的API,因此可能出現NotSupportException異常
    ThreadPool.QueueUserWorkItem(newWaitCallback(oAlpha.Beta),
    newSomeState(0));
    W2K =true;
    }
    catch(NotSupportedException)
    {
    Console.WriteLine("TheseAPI's may fail when called on a non-Windows 2000 system.");
    W2K =false;
    }
    if (W2K)//若是當前系統支持ThreadPool的方法.
    {
    for (intiItem=1;iItem < MaxCount;iItem++)
    {
      //插入隊列元素
      Console.WriteLine("Queueto Thread Pool {0}", iItem);
      ThreadPool.QueueUserWorkItem(newWaitCallback(oAlpha.Beta),new SomeState(iItem));
    }
    Console.WriteLine("Waitingfor Thread Pool to drain");
    file://等待事件的完成,即線程調用ManualResetEvent.Set()方法
    eventX.WaitOne(Timeout.Infinite,true);
    file://WaitOne()方法使調用它的線程等待直到eventX.Set()方法被調用
    Console.WriteLine("ThreadPool has been drained (Event fired)");
    Console.WriteLine();
    Console.WriteLine("Loadacross threads");
    foreach(objecto in oAlpha.HashCount.Keys)
    Console.WriteLine("{0}{1}", o, oAlpha.HashCount[o]);
    }
    Console.ReadLine();
    return 0;

  }
  }

  程序中有些小地方應該引發咱們的注意。SomeState類是一個保存信息的數據結構,在上面的程序中,它做爲參數被傳遞給每個線程,你很容易就能理解這個,由於你須要把一些有用的信息封裝起來提供給線程,而這種方式是很是有效的。程序出現的InterLocked類也是專爲多線程程序而存在的,它提供了一些有用的原子操做,所謂原子操做就是在多線程程序中,若是這個線程調用這個操做修改一個變量,那麼其餘線程就不能修改這個變量了,這跟lock關鍵字在本質上是同樣的。

  咱們應該完全地分析上面的程序,把握住線程池的本質,理解它存在的意義是什麼,這樣咱們才能駕輕就熟地使用它。下面是該程序的輸出結果:
  Thread PoolSample:
  Queuing 10items to Thread Pool
  Queue toThread Pool 0
  Queue toThread Pool 1
  ...
  ...
  Queue toThread Pool 9
  Waiting forThread Pool to drain
  98 0 :
  HashCount.Count==0,Thread.CurrentThread.GetHashCode()==98
  100 1 :
  HashCount.Count==1,Thread.CurrentThread.GetHashCode()==100
  98 2 :
  ...
  ...
  SettingeventX
  Thread Poolhas been drained (Event fired) 
  Load acrossthreads
  101 2
  100 3
  98 4
  102 1

  與ThreadPool類不一樣,Timer類的做用是設置一個定時器,定時執行用戶指定的函數,而這個函數的傳遞是靠另一個代理對象TimerCallback,它必須在建立Timer對象時就指定,而且不能更改。定時器啓動後,系統將自動創建一個新的線程,而且在這個線程裏執行用戶指定的函數。下面的語句初始化了一個Timer對象:
  Timer timer= new Timer(timerDelegate, s,1000, 1000); 

  第一個參數指定了TimerCallback代理對象;第二個參數的意義跟上面提到的WaitCallback代理對象的同樣,做爲一個傳遞數據的對象傳遞給要調用的方法;第三個參數是延遲時間——計時開始的時刻距如今的時間,單位是毫秒;第四個參數是定時器的時間間隔——計時開始之後,每隔這麼長的一段時間,TimerCallback所表明的方法將被調用一次,單位也是毫秒。這句話的意思就是將定時器的延遲時間和時間間隔都設爲1秒鐘。

  定時器的設置是能夠改變的,只要調用Timer.Change()方法,這是一個參數類型重載的方法,通常使用的原型以下:
   public boolChange(long, long);

  下面這段代碼將前邊設置的定時器修改了一下:
  timer.Change(10000,2000); 

  很顯然,定時器timer的時間間隔被從新設置爲2秒,中止計時10秒後生效。

  下面這段程序演示了Timer類的用法。

  usingSystem;
  usingSystem.Threading; 
  classTimerExampleState 
  {
  public intcounter = 0;
  public Timertmr;
  }

  class App 
  {
  publicstatic void Main()
  {
    TimerExampleStates = new TimerExampleState();

    //建立代理對象TimerCallback,該代理將被定時調用
    TimerCallbacktimerDelegate = new TimerCallback(CheckStatus);

    //建立一個時間間隔爲1s的定時器
    Timertimer = new Timer(timerDelegate, s,1000, 1000);
    s.tmr =timer;

    //主線程停下來等待Timer對象的終止
    while(s.tmr!= null)
    Thread.Sleep(0);
    Console.WriteLine("Timerexample done.");
    Console.ReadLine();
  }
  file://下面是被定時調用的方法

  static voidCheckStatus(Object state)
  {
    TimerExampleStates =(TimerExampleState)state;
    s.counter++;
    Console.WriteLine("{0}Checking Status {1}.",DateTime.Now.TimeOfDay, s.counter);
    if(s.counter== 5)
    {
    file://使用Change方法改變了時間間隔
    (s.tmr).Change(10000,2000);
    Console.WriteLine("changed...");
    }
    if(s.counter== 10)
    {
    Console.WriteLine("disposingof timer...");
    s.tmr.Dispose();
    s.tmr =null;
    }
  }
  }

  程序首先建立了一個定時器,它將在建立1秒以後開始每隔1秒調用一次CheckStatus()方法,當調用5次之後,在CheckStatus()方法中修改了時間間隔爲2秒,而且指定在10秒後從新開始。當計數達到10次,調用Timer.Dispose()方法刪除了timer對象,主線程因而跳出循環,終止程序。程序執行的結果以下:



  上面就是對ThreadPool和Timer兩個類的簡單介紹,充分利用系統提供的功能,能夠爲咱們省去不少時間和精力——特別是對很容易出錯的多線程程序。同時咱們也能夠看到.net Framework強大的內置對象,這些將對咱們的編程帶來莫大的方便。

  有 時候你會以爲上面介紹的方法好像不夠用,對,咱們解決了代碼和資源的同步問題,解決了多線程自動化管理和定時觸發的問題,可是如何控制多個線程相互之間的聯繫呢?例如我要到餐廳吃飯,在吃飯以前我先得等待廚師把飯菜作好,以後我開始吃飯,吃完我還得付款,付款方式能夠是現金,也能夠是信用卡,付款以後我才能離開。分析一下這個過程,我吃飯能夠看做是主線程,廚師作飯又是一個線程,服務員用信用卡收款和收現金能夠看做另外兩個線程,你們能夠很清楚地看到其中的關係——我吃飯必須等待廚師作飯,而後等待兩個收款線程之中任意一個的完成,而後我吃飯這個線程能夠執行離開這個步驟,因而我吃飯纔算結束了。事實上,現實中有着比這更復雜的聯繫,咱們怎樣才能很好地控制它們而不產生衝突和重複呢?

  這種狀況下,咱們須要用到互斥對象,即System.Threading命名空間中的Mutex類。你們必定坐過出租車吧,事實上咱們能夠把Mutex看做一個出租車,那麼乘客就是線程了,乘客首先得等車,而後上車,最後下車,當一個乘客在車上時,其餘乘客就只有等他下車之後才能夠上車。而線程與Mutex對象的關係也正是如此,線程使用Mutex.WaitOne()方法等待Mutex對象被釋放,若是它等待的Mutex對象被釋放了,它就自動擁有這個對象,直到它調用Mutex.ReleaseMutex()方法釋放這個對象,而在此期間,其餘想要獲取這個Mutex對象的線程都只有等待。

  下面這個例子使用了Mutex對象來同步四個線程,主線程等待四個線程的結束,而這四個線程的運行又是與兩個Mutex對象相關聯的。其中還用到AutoResetEvent類的對象,如同上面提到的ManualResetEvent對象同樣,你們能夠把它簡單地理解爲一個信號燈,使用AutoResetEvent.Set()方法能夠設置它爲有信號狀態,而使用AutoResetEvent.Reset()方法把它設置爲無信號狀態。這裏用它的有信號狀態來表示一個線程的結束。

  // Mutex.cs
  usingSystem;
  usingSystem.Threading;

  public classMutexSample
  {
  static MutexgM1;
  static MutexgM2;
  const intITERS = 100;
  staticAutoResetEvent Event1 = new AutoResetEvent(false);
  staticAutoResetEvent Event2 = new AutoResetEvent(false);
  staticAutoResetEvent Event3 = new AutoResetEvent(false);
  staticAutoResetEvent Event4 = new AutoResetEvent(false);

  publicstatic void Main(String[] args)
  {
    Console.WriteLine("MutexSample ...");
    //建立一個Mutex對象,而且命名爲MyMutex
    gM1 = newMutex(true,"MyMutex");
    //建立一個未命名的Mutex 對象.
    gM2 = newMutex(true);
    Console.WriteLine("- Main Owns gM1 and gM2");

    AutoResetEvent[]evs = new AutoResetEvent[4];
    evs[0] =Event1; file://爲後面的線程t1,t2,t3,t4定義AutoResetEvent對象
    evs[1] =Event2; 
    evs[2] = Event3;
    evs[3] =Event4; 

    MutexSampletm = new MutexSample( );
    Thread t1= new Thread(new ThreadStart(tm.t1Start));
    Thread t2= new Thread(new ThreadStart(tm.t2Start));
    Thread t3= new Thread(new ThreadStart(tm.t3Start));
    Thread t4= new Thread(new ThreadStart(tm.t4Start));
    t1.Start();// 使用Mutex.WaitAll()方法等待一個Mutex數組中的對象所有被釋放
    t2.Start();// 使用Mutex.WaitOne()方法等待gM1的釋放
    t3.Start();// 使用Mutex.WaitAny()方法等待一個Mutex數組中任意一個對象被釋放
    t4.Start();// 使用Mutex.WaitOne()方法等待gM2的釋放


    Thread.Sleep(2000);
    Console.WriteLine("- Main releases gM1");
    gM1.ReleaseMutex(); file://線程t2,t3結束條件知足

    Thread.Sleep(1000);
    Console.WriteLine("- Main releases gM2");
    gM2.ReleaseMutex(); file://線程t1,t4結束條件知足

    //等待全部四個線程結束
    WaitHandle.WaitAll(evs);
    Console.WriteLine("...Mutex Sample");
    Console.ReadLine();
  }

  public voidt1Start( )
  {
    Console.WriteLine("t1Startstarted, Mutex.WaitAll(Mutex[])");
    Mutex[]gMs = new Mutex[2];
    gMs[0] =gM1;//建立一個Mutex數組做爲Mutex.WaitAll()方法的參數
    gMs[1] =gM2;
    Mutex.WaitAll(gMs);//等待gM1和gM2都被釋放
    Thread.Sleep(2000);
    Console.WriteLine("t1Startfinished, Mutex.WaitAll(Mutex[]) satisfied");
    Event1.Set(); file://線程結束,將Event1設置爲有信號狀態
  }

  public voidt2Start( )
  {
    Console.WriteLine("t2Startstarted, gM1.WaitOne( )");
    gM1.WaitOne();//等待gM1的釋放
    Console.WriteLine("t2Startfinished, gM1.WaitOne( ) satisfied");
    Event2.Set();//線程結束,將Event2設置爲有信號狀態
  }

  public voidt3Start( )
  {
    Console.WriteLine("t3Startstarted, Mutex.WaitAny(Mutex[])");
    Mutex[]gMs = new Mutex[2];
    gMs[0] =gM1;//建立一個Mutex數組做爲Mutex.WaitAny()方法的參數
    gMs[1] =gM2;
    Mutex.WaitAny(gMs);//等待數組中任意一個Mutex對象被釋放
    Console.WriteLine("t3Startfinished, Mutex.WaitAny(Mutex[])");
    Event3.Set();//線程結束,將Event3設置爲有信號狀態
  }

  public voidt4Start( )
  {
    Console.WriteLine("t4Startstarted, gM2.WaitOne( )");
    gM2.WaitOne();//等待gM2被釋放
    Console.WriteLine("t4Startfinished, gM2.WaitOne( )");
    Event4.Set();//線程結束,將Event4設置爲有信號狀態
  }
  }

  下面是該程序的執行結果:
  從執行結果能夠很清楚地看到,線程t2,t3的運行是以gM1的釋放爲條件的,而t4在gM2釋放後開始執行,t1則在gM1和gM2都被釋放了以後才執行。Main()函數最後,使用WaitHandle等待全部的AutoResetEvent對象的信號,這些對象的信號表明相應線程的結束。

 

6、backgroundWorker多線程組件

在VS2005中添加了BackgroundWorker組件,該組件在多線程編程方面使用起來很是方便。BackgroundWorker類中主要用到的有這列屬性、方法和事件:
重要屬性:
一、CancellationPending獲取一個值,指示應用程序是否已請求取消後臺操做。經過在DoWork事件中判斷CancellationPending屬性能夠認定是否須要取消後臺操做(也就是結束線程);
二、IsBusy獲取一個值,指示 BackgroundWorker 是否正在運行異步操做。程序中使用IsBusy屬性用來肯定後臺操做是否正在使用中;
三、WorkerReportsProgress獲取或設置一個值,該值指示BackgroundWorker可否報告進度更新
四、WorkerSupportsCancellation獲取或設置一個值,該值指示 BackgroundWorker 是否支持異步取消。設置WorkerSupportsCancellation爲true使得程序能夠調用CancelAsync方法提交終止掛起的後臺操做的請求;
重要方法:
一、CancelAsync請求取消掛起的後臺操做
二、RunWorkerAsync開始執行後臺操做
三、ReportProgress引起ProgressChanged事件 
重要事件:
一、DoWork調用 RunWorkerAsync 時發生
二、ProgressChanged調用 ReportProgress 時發生
三、RunWorkerCompleted當後臺操做已完成、被取消或引起異常時發生
另外還有三個重要的參數是RunWorkerCompletedEventArgs以及DoWorkEventArgs、ProgressChangedEventArgs。
BackgroundWorker的各屬性、方法、事件的調用機制和順序:
                              
從上圖可見在整個生活週期內發生了3次重要的參數傳遞過程:
參數傳遞1:這次的參數傳遞是將RunWorkerAsync(Object)中的Object傳遞到DoWork事件的DoWorkEventArgs.Argument,因爲在這裏只有一個參數能夠傳遞,因此在實際應用往封裝一個類,將整個實例化的類做爲RunWorkerAsync的Object傳遞到DoWorkEventArgs.Argument;
參數傳遞2:這次是將程序運行進度傳遞給ProgressChanged事件,實際使用中每每使用給方法和事件更新進度條或者日誌信息;
參數傳遞3:在DoWork事件結束以前,將後臺線程產生的結果數據賦給DoWorkEventArgs.Result一邊在RunWorkerCompleted事件中調用RunWorkerCompletedEventArgs.Result屬性取得後臺線程產生的結果。
另外從上圖能夠看到DoWork事件是在後臺線程中運行的,因此在該事件中不可以操做用戶界面的內容,若是須要更新用戶界面,可使用ProgressChanged事件及RunWorkCompleted事件來實現。

明白了BagkgroundWorker的事件調用順序和參數傳遞機制以後在使用該組件用於多線程編程的時候就能夠輕鬆許多了。

下面看我寫的一個demo

//咱們假設獲取的記錄數固定,咱們爲此定義一個常量:
private static int MaxRecords = 100;

private void Form1_Load(object sender,EventArgs e)
{

}

/// <summary>
/// 開始
/// </summary>
private void button1_Click(object sender, EventArgs e)
{
if (this.backgroundWorker1.IsBusy)
{
MessageBox.Show("正在執行");
return;
}
this.textBox1.Clear();
//當Start按鈕被點擊後,RunWorkerAsync方法被掉調用,咱們定義的常量(MaxRecords )看成參數被摻入。
//隨後,將會觸發其DoWork事件
this.backgroundWorker1.RunWorkerAsync(MaxRecords);
this.button1.Enabled = false;
this.button2.Enabled = true;
}

private voidbackgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
try
{
//調用RetrieveData方法逐條獲取數據
e.Result = this.RetrieveData(this.backgroundWorker1, e);
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
throw;
}
}

private int RetrieveData(BackgroundWorkerworker, DoWorkEventArgs e)
{
int maxRecords = (int)e.Argument;
int percent = 0;
for (int i = 1; i <= maxRecords; i++)
{
if (worker.CancellationPending)
{
return i;
}

percent = (int)(((double)i /(double)maxRecords) * 100);
worker.ReportProgress(percent, new KeyValuePair<int, string>(i,Guid.NewGuid().ToString()));
System.Threading.Thread.Sleep(100);
}

return maxRecords;
}

//這些操做須要操做UI上的控件,只能在MainThread中進行。
//若是在RetrieveData方法進行的話,因爲該方式是一個異步方法,是會拋出異常的
private void backgroundWorker1_ProgressChanged(object sender,ProgressChangedEventArgs e)
{
KeyValuePair<int, string> record = (KeyValuePair<int,string>)e.UserState;
this.label1.Text = string.Format("There are {0} records retrieved!",record.Key);
this.progressBar1.Value = e.ProgressPercentage;
this.textBox1.AppendText(record.Value+"\r\n");
}

private void button2_Click(object sender,EventArgs e)
{
this.backgroundWorker1.CancelAsync();//請求結束後臺操做
}
//若是操做正常地結束,BackgroundWorker的RunWorkerCompleted會被觸發
private void backgroundWorker1_RunWorkerCompleted(object sender,RunWorkerCompletedEventArgs e)
{
try
{
this.label1.Text = string.Format("Total records: {0}", e.Result);
this.button1.Enabled = true;
this.button2.Enabled = false;
}
catch (TargetInvocationException ex)
{
MessageBox.Show(ex.InnerException.GetType().ToString());
}
}

 

七、注意事項

一、只要有一個前臺線程在運行,應用程序的進程就在運行。若是多個前臺線程在運行,而Main方法結束了,應用程序的進程就是激活的。直到全部前臺線程完成其任務爲止。在默認狀況下,用Thread類建立的線程是前臺線程;線程池中的線程老是後臺線程。

二、只有引用對象才能用於鎖。關於lock(this), lock(typeof(ClassName))以及lock("thisLock")的討論。

MSDN上指出,使用lock時不注意可能致使如下問題:

1). 若是實例能夠被公共訪問,將出現 lock (this) 問題。

2). 若是 MyType 能夠被公共訪問,將出現 lock (typeof (MyType)) 問題。

3). 因爲進程中使用同一字符串的任何其餘代碼都將共享同一個鎖,因此出現 lock(「myLock」) 問題。

下面來討論這些問題:

首先看lock(this)問題,若是一個類是公有的時,應該避免在基方法或屬性中使用lock(this)語句,由於若是有其餘人使用你的組件,它並不瞭解你的組件內部是否使用了鎖,若是使用了而使用者又在類外部對類實例嘗試加鎖,則可能致使一個死鎖,下面是我從Google(原地址:http://www.toolazy.me.uk/template.php?content=lock(this)_causes_deadlocks.xml,原程序中因爲將主線程睡眠,實際操做系統會自動切換可用線程,於是未能體現出死鎖的效果)找到的修改後的例子:

using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;

namespace Test
{
class InternalClass
{
public void TryLockThis()
{
Thread t = new Thread(ThreadFunction);
t.Start();
}

private void ThreadFunction()
{
Thread.Sleep(3000); // 延遲,等待外部對對象實例加鎖
Console.WriteLine("嘗試經過lock(this)加鎖下面的代碼塊...");

while (true)
{
lock (this)
{
Console.WriteLine("執行內部鎖,1秒後繼續...");
Thread.Sleep(1000);
Console.WriteLine("內部鎖完成...");
}
}
}
}


class ClassMain
{
private InternalClass theClass = new InternalClass();

public ClassMain()
{
theClass.TryLockThis();
Console.WriteLine("在執行內部鎖以前對對象進行加鎖...");
lock (theClass) // 若是註釋掉這句,ThreadFunction()中的lock將執行成功
{
Console.WriteLine("對象被鎖定, 在這裏咱們得到了一個死鎖...");

while (true) { }
}
}

[STAThread]
static void Main(string[] args)
{
ClassMain cm = new ClassMain();

Console.WriteLine("Press Enter toexit");
Console.ReadLine();
}
}
}

能夠看到上述的程序會致使一個死鎖,程序的執行結果是這樣的:

在執行內部鎖以前對對象進行加鎖...
對象被鎖定, 在這裏咱們得到了一個死鎖...
嘗試經過lock(this)加鎖下面的代碼塊...

(能夠這樣使用:object ob = new object(); lock(ob) ....)

所以,應儘可能避免甚至拒絕使用lock(this)這樣的語句。

一樣的,lock(typeof(ClassName))也有相似的問題,因爲typeof語句返回的是一個類的類型實例,對於一個類來講只有一個,若是在類的方法或實例方法中使用了typeof(className)這樣的語句,而在類的外部又嘗試對類進行加鎖,一樣可能致使死鎖。

關於lock("thisLock")的問題,是由.Net的字符串處理模式致使的。
[MSDN]
公共語言運行庫經過維護一個表來存放字符串,該表稱爲拘留池,它包含程序中以編程方式聲明或建立的每一個惟一的字符串的一個引用。所以,具備特定值的字符串的實例在系統中只有一個。
例如,若是將同一字符串分配給幾個變量,運行庫就會從拘留池中檢索對該字符串的相同引用,並將它分配給各個變量。

因爲這個緣由,假如你lock的字符串對象在拘留池中(一般是編譯時顯式聲明的用引號引發來的字符串),那麼,這個對象也有可能被分配到另一個變量,若是這個變量也使用了lock,那麼,兩個lock語句表面上lock的不一樣對象,但實質上倒是同一個對象,這將形成沒必要要的阻塞,甚至可能形成死鎖。且不亦發現問題。

從下面的程序中能夠看出一個問題:
string a = "String Example";
string b = "String Example";
string c = (new StringBuilder()).Append("String Example").ToString();
Console.WriteLine("a==b? {0}", object.ReferenceEquals(a, b));
Console.WriteLine("a==c? {0}", object.ReferenceEquals(a, c));

上面程序執行的結果是:
a==b? True
a==c? False

從上面能夠看出,a和b指向的是同一個引用,而lock正是經過引用來區分並加鎖臨界代碼段的。也就是說,若是在咱們一程序的一個部分中使用了lock("thisLock")進行加鎖,而在程序的另外一個位置一樣也使用lock("thisLock")進行加鎖,則極有可能致使一個死鎖,於是是很危險的。實際上,不僅是lock("thisLock")這樣的語句,在lock中使用string類型的引用都有可能致使死鎖。

上述就是C#中應避免使用lock(this),lock(typeof(className)), lock("thisLock")的緣由。

三、線程的基本操做,例如:暫停、繼續、中止等?

我不建議使用Thread類提供的Suspend、Resume以及Abort這三個方法,前兩個有問題,好像在VS05已經屏蔽這兩個方法;對於Abort來講,除了資源沒有獲得及時釋放外,有時候會出現異常。如何作呢,經過設置開關變量來完成。

四、如何向線程傳遞參數或者從中獲得其返回值?

我不建議使用靜態成員來完成,僅僅爲了線程而破壞類的封裝有些得不償失。那如何作呢,經過建立單獨的線程類來完成。

五、如何使線程所佔用的CPU不要總是百分之百?

形成這個緣由是因爲線程中進行不間斷的循環操做,從而使CPU徹底被子線程佔有。那麼處理此類問題,其實很簡單,在適當的位置調用Thread.Sleep(20)來釋放所佔有CPU資源,不要小看這20毫秒的睡眠,它的做用但是巨大的,可使其餘線程獲得CPU資源,從而使你的CPU使用效率降下來。

六、如何在子線程中控制窗體控件?

爲何不能直接在子線程中操縱UI呢。緣由在於子線程和UI線程屬於不一樣的上下文,換句比較通俗的話說,就比如兩我的在不一樣的房間裏同樣,那麼要你直接操做另外一個房間裏的東西,恐怕不行罷,那麼對於子線程來講也同樣,不能直接操做UI線程中的對象。

那麼如何在子線程中操縱UI線程中的對象呢,.Net提供了Invoke和BeginInvoke這兩種方法。簡單地說,就是子線程發消息讓UI線程來完成相應的操做。

這兩個方法有什麼區別,Invoke須要等到所調函數的返回,而BeginInvoke則不須要。

用這兩個方法須要注意的,有以下三點:

第一個是因爲Invoke和BeginInvoke屬於Control類型的成員方法,所以調用的時候,須要獲得Control類型的對象才能觸發,也就是說你要觸發窗體作什麼操做或者窗體上某個控件作什麼操做,須要把窗體對象或者控件對象傳遞到線程中。

第二個,對於Invoke和BeginInvoke接受的參數屬於一個delegate類型,我在之前的文章中使用的是MethodInvoker,這是.Net自帶的一個delegate類型,而並不意味着在使用Invoke或者BeginInvoke的時候只能用它。

第三個,使用Invoke和BeginInvoke有個須要注意的,就是當子線程在Form_Load開啓的時候,會遇到異常,這是由於觸發Invoke的對象尚未徹底初始化完畢。處理此類問題,在開啓線程以前顯式的調用「this.Show();」,來使窗體顯示在線程開啓以前。若是此時只是開啓線程來初始化顯示數據,那我建議你不要使用子線程,用Splash窗體的效果可能更好。這方面能夠參看以下的例子。

http://www.syncfusion.com/FAQ/WindowsForms/FAQ_c95c.aspx#q621q

七、線程同步lock,Monitor,同步事件EventWaitHandler,互斥體Mutex的基本用法,在此基礎上,咱們對它們用法進行比較,並給出何時須要鎖何時不須要的幾點建議。最後,介紹幾個FCL中線程安全的類,集合類的鎖定方式等,作爲對線程同步系列的完善和補充。

1).幾種同步方法的區別 lock和Monitor是.NET用一個特殊結構實現的,Monitor對象是徹底託管的、徹底可移植的,而且在操做系統資源要求方面可能更爲有效,同步速度較快,但不能跨進程同步。lock(Monitor.Enter和Monitor.Exit方法的封裝),主要做用是鎖定臨界區,使臨界區代碼只能被得到鎖的線程執行。Monitor.Wait和Monitor.Pulse用於線程同步,相似信號操做,我的感受使用比較複雜,容易形成死鎖。互斥體Mutex和事件對象EventWaitHandler屬於內核對象,利用內核對象進行線程同步,線程必需要在用戶模式和內核模式間切換,因此通常效率很低,但利用互斥對象和事件對象這樣的內核對象,能夠在多個進程中的各個線程間進行同步。 互斥體Mutex相似於一個接力棒,拿到接力棒的線程才能夠開始跑,固然接力棒一次只屬於一個線程(Thread Affinity),若是這個線程不釋放接力棒(Mutex.ReleaseMutex),那麼沒辦法,其餘全部須要接力棒運行的線程都知道能等着看熱鬧。 EventWaitHandle 類容許線程經過發信號互相通訊。一般,一個或多個線程在 EventWaitHandle 上阻止,直到一個未阻止的線程調用 Set 方法,以釋放一個或多個被阻止的線程。

 2).何時須要鎖定 首先要理解鎖定是解決競爭條件的,也就是多個線程同時訪問某個資源,形成意想不到的結果。好比,最簡單的狀況是,一個計數器,兩個線程同時加一,後果就是損失了一個計數,但至關頻繁的鎖定又可能帶來性能上的消耗,還有最可怕的狀況死鎖。那麼什麼狀況下咱們須要使用鎖,什麼狀況下不須要呢? 1)只有共享資源才須要鎖定 只有能夠被多線程訪問的共享資源才須要考慮鎖定,好比靜態變量,再好比某些緩存中的值,而屬於線程內部的變量不須要鎖定。 2)多使用lock,少用Mutex 若是你必定要使用鎖定,請儘可能不要使用內核模塊的鎖定機制,好比.NET的Mutex,Semaphore,AutoResetEvent和ManuResetEvent,使用這樣的機制涉及到了系統在用戶模式和內核模式間的切換,性能差不少,可是他們的優勢是能夠跨進程同步線程,因此應該清楚的瞭解到他們的不一樣和適用範圍。 3)瞭解你的程序是怎麼運行的 實際上在web開發中大多數邏輯都是在單個線程中展開的,一個請求都會在一個單獨的線程中處理,其中的大部分變量都是屬於這個線程的,根本沒有必要考慮鎖定,固然對於ASP.NET中的Application對象中的數據,咱們就要考慮加鎖了。 4)把鎖定交給數據庫 數據庫除了存儲數據以外,還有一個重要的用途就是同步,數據庫自己用了一套複雜的機制來保證數據的可靠和一致性,這就爲咱們節省了不少的精力。保證了數據源頭上的同步,咱們多數的精力就能夠集中在緩存等其餘一些資源的同步訪問上了。一般,只有涉及到多個線程修改數據庫中同一條記錄時,咱們才考慮加鎖。 5)業務邏輯對事務和線程安全的要求 這條是最根本的東西,開發徹底線程安全的程序是件很費時費力的事情,在電子商務等涉及金融系統的案例中,許多邏輯都必須嚴格的線程安全,因此咱們不得不犧牲一些性能,和不少的開發時間來作這方面的工做。而通常的應用中,許多狀況下雖然程序有競爭的危險,咱們仍是能夠不使用鎖定,好比有的時候計數器少一多一,對結果無傷大雅的狀況下,咱們就能夠不用去管它。

3).InterLocked類 Interlocked 類提供了同步對多個線程共享的變量的訪問的方法。若是該變量位於共享內存中,則不一樣進程的線程就可使用該機制。互鎖操做是原子的,即整個操做是不能由相同變量上的另外一個互鎖操做所中斷的單元。這在搶先多線程操做系統中是很重要的,在這樣的操做系統中,線程能夠在從某個內存地址加載值以後可是在有機會更改和存儲該值以前被掛起。 咱們來看一個InterLock.Increment()的例子,該方法以原子的形式遞增指定變量並存儲結果,示例以下:  Increment()方法累加的示例 classInterLockedTest { public staticInt64 i = 0;public staticvoid Add() { for (int i = 0; i < 100000000; i++) {Interlocked.Increment(ref InterLockedTest.i); //InterLockedTest.i= InterLockedTest.i + 1; } } public static void Main(string[] args) { Thread t1= new Thread(newThreadStart(InterLockedTest.Add)); Thread t2 = new Thread(new ThreadStart(InterLockedTest.Add)); t1.Start(); t2.Start();t1.Join(); t2.Join(); Console.WriteLine(InterLockedTest.i.ToString());Console.Read(); } } 輸出結果200000000,若是InterLockedTest.Add()方法中用註釋掉的語句代替Interlocked.Increment()方法,結果將不可預知,每次執行結果不一樣。InterLockedTest.Add()方法保證了加1操做的原子性,功能上至關於自動給加操做使用了lock鎖。同時咱們也注意到InterLockedTest.Add()用時比直接用+號加1要耗時的多,因此說加鎖資源損耗仍是很明顯的。 另外InterLockedTest類還有幾個經常使用方法,具體用法能夠參考MSDN上的介紹。

 4).集合類的同步 .NET在一些集合類,好比Queue、ArrayList、HashTable和Stack,已經提供了一個供lock使用的對象SyncRoot。用Reflector查看了SyncRoot屬性(Stack.SynchRoot略有不一樣)的源碼以下:

SyncRoot屬性源碼

public virtual object SyncRoot

{

 get

{ if (this._syncRoot == null)

{ //若是_syncRoot和null相等,將new object賦值給_syncRoot //Interlocked.CompareExchange方法保證多個線程在使用syncRoot時是線程安全的 Interlocked.CompareExchange(ref this._syncRoot,new object(), null);

}

 return this._syncRoot;

}

 }

這裏要特別注意的是MSDN提到:從頭至尾對一個集合進行枚舉本質上並非一個線程安全的過程。即便一個集合已進行同步,其餘線程仍能夠修改該集合,這將致使枚舉數引起異常。若要在枚舉過程當中保證線程安全,能夠在整個枚舉過程當中鎖定集合,或者捕捉因爲其餘線程進行的更改而引起的異常。應該使用下面的代碼:

Queue使用lock示例

 Queue q = new Queue();

 lock (q.SyncRoot)

{

foreach (object item in q)

{ //do something }

}

還有一點須要說明的是,集合類提供了一個是和同步相關的方法Synchronized,該方法返回一個對應的集合類的wrapper類,該類是線程安全的,由於他的大部分方法都用lock關鍵字進行了同步處理。如HashTable的Synchronized返回一個新的線程安全的HashTable實例,代碼以下:

Synchronized的使用和理解 //在多線程環境中只要咱們用下面的方式實例化HashTable就能夠了 Hashtable ht = Hashtable.Synchronized(newHashtable());

 //如下代碼是.NET Framework Class Library實現,增長對Synchronized的認識 [HostProtection(SecurityAction.LinkDemand,Synchronization=true)]

 public static Hashtable Synchronized(Hashtable table)

 {

if (table == null)

{ throw new ArgumentNullException("table"); }

return new SyncHashtable(table);

 } //SyncHashtable的幾個經常使用方法,咱們能夠看到內部實現都加了lock關鍵字保證線程安全public override void Add(object key, object value)

{

lock (this._table.SyncRoot)

{ this._table.Add(key,value); }

 }

public override void Clear()

 {

 lock (this._table.SyncRoot)

 { this._table.Clear(); }

}

public override void Remove(object key)

{

lock (this._table.SyncRoot) { this._table.Remove(key);}

 }

線程同步是一個很是複雜的話題,這裏只是根據公司的一個項目把相關的知識整理出來,做爲工做的一種總結。這些同步方法的使用場景是怎樣的?究竟有哪些細微的差異?還有待於進一步的學習和實踐。

相關文章
相關標籤/搜索