[原譯]多線程揭秘


多線程揭秘

Demo下載:多線程演示Demo

介紹

本文將通過一些例子來展示.net 中如何實現多線程,涉及到以下四部分。

1 .線程概念

2 .如何實現多線程

3 .如何確保線程安全

4 .死鎖

 

什么是進程

一個進程就是一個可執行文件運行的操作系統上下文環境。它被用來分隔虛擬地址空間,線程,對象句柄(指向類似文件這樣的資源的指針),以及環境變量,進程還有一些類似優先級類和最大內存分配的屬性。

 

也就是說:

1 .一個進程就是一個包含資源的內存塊。

2 .操作系統執行的一個單獨的任務。

3 .一個正在運行的軟件

4 .一個進程擁有一個/多個操作系統線程

 

一般的。一個進程最大可以是4GB的內存空間,這塊內存是安全,私有,其他進程是無法訪問的。

 

什么是線程

一個線程就是在一個進程里執行的一條指令流,所有的線程都在一個進程里執行,也就是一個進程可以包含多個線程。線程公用進程的虛擬地址空間。線程是操作系統的調度單元。一個線程的上下文由操作系統進行保存/恢復。

也就是說:

1 .一個線程是進程里的一條指令流。

2 .所有的線程在進程里。一個進程可以有多個線程

3 .一個進程的所有線程使用進程的虛擬地址空間。

什么是多線程

多線程指的是進程同時有多個線程活動。這可以通過時間片的線程模擬或是多cpu上的超線程來實現。可以提高性能。

多線程-為什么或是為什么不?

為什么多線程

1 .保持UI響應。

2 .提高性能(對於cpu密集型和I/O密集型的進程)

為什么不多線程

1 .過度使用降低性能

2 .代碼復雜,增加設計時間,潛在的bug

 

線程池

線程池為你的程序提供了一個由操作系統管理的機制。在線程池里的都是后台線程。一個線程池線程在程序的前台線程都退出后,也會推出。每個進程一個線程池。默認情況下。每個處理器會為進程分配25個線程。但是可以通過SetMaxThreads  方法來改變。

 

.net 中的線程

在.net 中,線程可以通過下面6個方法來實現。

1 .Thread線程類

2 .Delegates委托

3 .Background Worker

4 .ThreadPool 線程池

5 .Task任務類

6 .Parallel並行類

 

下面的幾部分里。我將逐一展示實現方法。

 

簡而言之,多線程就是通過使程序同時運行多個任務來最大化計算機能力,同時能夠保持UI響應。下圖是一個例子的圖示。

 

 

代碼

提供的源碼是一個簡單的WinForm程序。模擬了.net中委托,線程類和Background Worker三種方法。

程序異步執行一個繁重的操作,這樣UI就不會無響應。三個方法都是模擬的。

 

 

這個“繁重”的操作

真實的開發中,這個繁重的操作從輪詢數據庫到流媒體操作都可以。基本上可以是任何事情。源碼里面是向一個字符串追加值。String是不能變的。追加的時候,新的字符串變量會被創建,舊的會被丟棄,這是由CLR處理的。如果做很多次這個操作,是很耗資源的。這也是為什么我們使用Stringbuilder.Append 來代替這個操作。通過調整界面中的次數。可以通知追加的次數。

 

后面我們有一個Utility淚,有一個LoadData() 方法。類里面也有一個和LoadData() 有着同樣簽名的委托

class Utility

{

    public delegate string delLoadData(int number);

    public static delLoadData dLoadData;

 

    public Utility()

    {

       

    }

 

    public static string LoadData(int max)

    {

        string str = string.Empty;

 

        for (int i = 0; i < max; i++)

                                {

            str += i.ToString();

                                }

 

        return str;

    }

}

 

 

同步調用

當點擊Get Data Sync按鈕的時候。操作和UI在同一個線程里,因此阻塞了UI線程。因此。UI線程會未響應

private void btnSync_Click(object sender, EventArgs e)
{
    this.Cursor = Cursors.WaitCursor;
    this.txtContents.Text = Utility.LoadData(upCount);
    this.Cursor = Cursors.Default;
}

 

異步調用

使用委托(異步編程模型)

 

如果你選擇了“Delegates”單選按鈕,那么LoadData()方法就會通過使用委托來異步調用。首先通過utility.LoadData(). 的地址初始化delLoadData  類型,然后調用委托的BeginInvoke()方法。在.net的世界里。任何一個有着BeginXXX和EndXXX名字的方法都是異步的。比如delegate.Invoke()將會在同一個線程里調用方法。而delegate.BeginInvoke()則會另開一個線程調用。

BeginInvoke()有三個參數

1 .傳遞給Utility.LoadData()方法的參數

2 .回調方法的地址

3 .對象的狀態

Utility.dLoadData = new Utility.delLoadData(Utility.LoadData);
Utility.dLoadData.BeginInvoke(upCount, CallBack, null);

 

回調

一旦我們開了一個線程執行一些操作,我們就想知道操作正在發生些什么,換句話說。我們需要當操作完成的時候我們能夠收到通知。有三種方法可以知道一個操作是否完成。

1 .回調

2 .輪詢

3 .等待直到完成

在我的源碼里,我們使用回調方法來捕獲線程的完成。回調只需要在調用BeginInvoke的時候把回到函數的名字傳遞進去。這會告訴線程當你做完工作以后調用這個回調方法就好了。

 

一旦一個獨立線程里的一個方法啟動。你也許關心也許不關心方法的返回值,如果一個方法沒有返回值,那么可以叫做“觸發然后忘記的調用”,這種情況下就不需要回調函數了。這里callback直接傳入null就可以了。

Utility.dLoadData.BeginInvoke(upCount, CallBack, null);

 

在我們的例子中,我們需要一個回調方法,因此,哦們需要傳遞回調方法的名字到參數里。這里我們的回調方法的名字就叫做CallBack(),純屬巧合。

private void CallBack(IAsyncResult asyncResult)
{
    string result= string.Empty;
 
    if (this.cancelled)
        result = "Operation Cancelled";
    else
        result = Utility.dLoadData.EndInvoke(asyncResult);
    
      object[] args = { this.cancelled, result };
    this.BeginInvoke(dUpdateUI, args);
}

 

 

回調方法的簽名都是void MethodName(IAsyncResult asyncResult).

IAsyncResult包含了關於線程的一些必要的信息,返回的數據可以像下面這樣提取。

result = Utility.dLoadData.EndInvoke(asyncResult);

 

 

而輪詢的方法(本例沒有使用)則是像這樣

IAsyncResult r = Utility.dLoadData.BeginInvoke(upCount, CallBack, null);
while (!r.IsCompleted)
{
    //do work
}
result = Utility.dLoadData.EndInvoke(asyncResult);

 

等待直到完成,如名所示,就是等待直到完成。

IAsyncResult r = Utility.dLoadData.BeginInvoke(upCount, CallBack, null);
 
//do work
result = Utility.dLoadData.EndInvoke(asyncResult);

 

 

更新UI

既然我們已經捕獲了操作結束,並且取回了LoadData()的結果。我們需要用結果來更新UI,但是有個問題。文本框需要在UI線程里更新,結果在回調里取到了。回調和他啟動的時候是一個線程(他是由新開的線程啟動的)。因為UI線程和回調不是同一個線程。換句話說。文本框不能像下面這樣更新。

this.txtContents.Text = text;

 

 

回調里執行這一行將會導致一個跨線程的系統異常。我們需要在后台線程和Ui線程之前構建一個橋。來更新文本框的值。可以通過使用Invoke()或是BeginInvoke()方法。

我定義了一個方法來更新UI

private void UpdateUI(bool cancelled, string text)
{
    this.btnAsync.Enabled = true;
    this.btnCancel.Enabled = false;
    this.txtContents.Text = text;
}

 

對上面的方法定義一個委托

private delegate void delUpdateUI(bool value, string text);
dUpdateUI = new delUpdateUI(UpdateUI);

 

如下調用BeginInvoke()方法。

object[] args = { this.cancelled, result };
this.BeginInvoke(dUpdateUI, args);

 

需要注意的是一旦一個線程通過委托啟動。它就不能取消,暫停,或是終止,我們無法控制那個線程。

 

使用Thread線程類

同樣的操作可以是喲哦那個Thread類來完成。這個類的優點是你可以對操作有更多的控制,比如暫停/取消操作,類在System.Threading命名空間里。

我們有一個私有的方法LoadData(),他是Utility.LoadData()方法的一個包裝。

private void LoadData()
{
    string result = Utility.LoadData(upCount);
    object[] args = { this.cancelled, result };
    this.BeginInvoke(dUpdateUI, args);
}

 

這樣做是因為 Utility.LoadData() 需要一個參數。而我們需要一個ThreadStart委托,這個委托沒有參數。

doWork = new Thread(new ThreadStart(this.LoadData));
doWork.Start();

 

這個委托沒有參數,為了防止我們需要傳遞參數,我們可以使用有參的ThreadStart委托,不幸的是,這個委托只能把object作為參數,而我們需要一個字符串所以需要類型轉換。

doWork = new Thread(new ParameterizedThreadStart(this.LoadData));
doWork.Start(parameter);

 

是的。Thread淚可以對線程有更多的控制。中斷。終止,獲取線程狀態。

使用BackgroundWorker

這個類是一個組件,可以使得線程使用更簡單,這個BackgroundWorker類的主要特點就是可以異步的報告進度,這就可以用來更新狀態欄,保持UI可視化的更新進度

為了完成操作,我們需要把下面兩個屬性設置為true,缺省時false

  • WorkerReportsProgress
  • WorkerSupportsCancel

 

這個類有三個主要的事件DoCountProgressChangedRunWorkerCompleted 初始化的時候需要注冊這三個事件

this.bgCount.DoWork += new DoWorkEventHandler(bgCount_DoWork);

this.bgCount.ProgressChanged +=

     new ProgressChangedEventHandler(bgCount_ProgressChanged);

this.bgCount.RunWorkerCompleted +=

     new RunWorkerCompletedEventHandler(bgCount_RunWorkerCompleted);

 

 

通過調用RunWorkerAsync() 方法來啟動操作

this.bgCount.RunWorkerAsync();

 

一旦調用,下面的方法就會啟動來執行操作。

void bgCount_DoWork(object sender, DoWorkEventArgs e)

{

    string result = string.Empty;

    if (this.bgCount.CancellationPending)

    {

        e.Cancel = true;

        e.Result = "Operation Cancelled";

    }

    else

    {

        for (int i = 0; i < this.upCount; i++)

        {

            result += i.ToString();

            this.bgCount.ReportProgress((i / this.upCount) * 100);

        }

        e.Result = result;

    }

}

 

 

CancellationPending  屬性用來檢查該操作是否被取消。要取消操作,需要調用

this.bgCount.CancelAsync();

 

下面這行代碼報告進度

this.bgCount.ReportProgress((i / this.upCount) * 100);

 

一旦調用,下面的方法就會被調用,來更新UI

void bgCount_ProgressChanged(object sender, ProgressChangedEventArgs e)

{

    if (this.bgCount.CancellationPending)

        this.txtContents.Text = "Cancelling....";

    else

        this.progressBar.Value = e.ProgressPercentage;

}

 

最后,操作完成時調用bgCount_RunWorkerCompleted  方法

void bgCount_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)

{

    this.btnAsync.Enabled = true;

    this.btnCancel.Enabled = false;

    this.txtContents.Text = e.Result.ToString();

}

 

 

Thread Pool線程池

 

不建議程序員隨心所欲創建很多線程,創建線程是很昂貴的操作,有一些額外的調用。同時,每個cpu在一個時間片內只能運行一個建成,如果一個單核系統上有多個線程,計算機一次只能運行一個,因此通過給線程分配時間片來模擬多線程。會產生上下文切換的消費,因此,如果有很多的線程,其中一些什么都不做,保持閑置,那么這些額外的消費會影響性能。因此,程序員對於創建線程應該相當小心

 

幸運的是,CLR有一個托管的代碼庫。這就是ThreadPool類,這個類管理一些線程,並且根據我們的程序創建或銷毀線程,開始的時候沒有線程啟動。當需要的時候就會創建,如果我們設置了SetMinThreads屬性,一旦開始操作就很快會達到這個值,之后,如果發現有些線程閑置了很長時間,則會決定會殺掉一些線程。

線程池類液允許我們管理一系列的工作項目。這些工作項目會委托到一個后台線程。

WaitCallback threadCallback = new WaitCallback(HeavyOperation);
 
for (int i = 0; i < 3; i++)
{
  System.Threading.ThreadPool.QueueUserWorkItem(HeavyOperation, i);                           
}

其中heavyOperation定義如下:

private static void HeavyOperation(object WorkItem)
{
  System.Threading.Thread.Sleep(5000);
  Console.WriteLine("Executed work Item {0}", (int)WorkItem);
} 

注意WaitCallBack這個委托的簽名,需要把一個object作為參數,通常用來在線程間傳遞狀態信息。

注意我們知道委托通過使用ThreadPool來工作。我們必須探索和他一起的回調技術,我們可以使用WaitHandle來捕獲回調,WaitHandle派生了兩個子類:

 AutoResetEvent 和 ManualResetEvent. 

public static void Demo_ResetEvent()
{  
  Server s = new Server();
  ThreadPool.QueueUserWorkItem(new WaitCallback((o) =>
  {
     s.DoWork();                
 
   }));
 
   ((AutoResetEvent)Global.GetHandle(Handles.AutoResetEvent)).WaitOne();
    Console.WriteLine("Work complete signal received");
} 

 

這里有一個Global類,這個類包含WaitHandles的一個單例。

public static class Global
{
  static WaitHandle w = null;
  static AutoResetEvent ae = new AutoResetEvent(false);
  static ManualResetEvent me = new ManualResetEvent(false);
  public static WaitHandle GetHandle(Handles Type)
  {            
    switch (Type)
    {                
      case Handles.ManualResetEvent:                    
         w = me;
         break;
      case Handles.AutoResetEvent:                    
         w = ae;                    
         break;
      default:
         break;
    }
    return w;
  }
}  

 

而WaitOne方法。阻塞了代碼執行,直到在后台線程中設置了WaitHandle。

public void DoWork()
{            
  Console.WriteLine("Work Starting ...");
  Thread.Sleep(5000);
  Console.WriteLine("Work Ended ...");
  ((AutoResetEvent)Global.GetHandle(Handles.AutoResetEvent)).Set();
} 

 

AutoResetEvent當自動設置以后又重設自己。和高速收費站很類似。多輛車合並,以讓一次只有一輛車通過。當一輛車來的時候,門就設置為允許通過,然后又重設為關閉處理下一輛車。

下面的例子詳細說明了AutoResetEvent。想一想。我們有一個服務名為DoWork()這個方法就是繁重的操作,我們的程序需要在調用這個方法后更新日志文件。考慮到多個線程異步的訪問這個方法。我們必須確保更新日志文件是線程安全的,這樣一次只能有一個線程可用。

public void DoWork(int threadID, int waitSingal)
{ 
  Thread.Sleep(waitSingal);
  Console.WriteLine("Work Complete by Thread : {0} @ {1}", threadID, DateTime.Now.ToString("hh:mm:ss"));
  ((AutoResetEvent)Global.GetHandle(Handles.AutoResetEvent)).Set();
 
} 
 

public void UpdateLog(int threadID)
{
  if(((AutoResetEvent)Global.GetHandle(Handles.AutoResetEvent)).WaitOne(5000))
       Console.WriteLine("Update Log File by thread : {0} @ {1}", threadID, DateTime.Now.ToString("hh:mm:ss"));
  else
       Console.WriteLine("Time out");
}
 

 

我們創建兩個線程,同時委托DoWork()方法。然后我們調用UpdateLog()方法。更新日志的代碼執行將會等待直到每一個線程都完成各自的工作之后才執行。

public static void Demo_AutoResetEvent()
{
  Console.WriteLine("Demo Autoreset event...");
  Server s = new Server();
 
  Console.WriteLine("Start Thread 1..");
  ThreadPool.QueueUserWorkItem(new WaitCallback((o) =>
  {
     s.DoWork(1, 4000);  
                
  }));            
 
  Console.WriteLine("Start Thread 2..");
  ThreadPool.QueueUserWorkItem(new WaitCallback((o) =>
  {
     s.DoWork(2, 4000);                
                
  }));
 
  s.UpdateLog(1);
  s.UpdateLog(2);
} 
 

 

ManualResetEvent 不同於AutoResetEvent,我們需要在再次設置之前手工重置他,他不會自動重置,考慮我們有一個方法是持續在后台線程中發布消息。這個方法持續循環等待信號以發送消息。當值被設置以后,方法就開始發消息。當等待句柄被重置,發送服務停止,然后處理就可以重復進行了。

public void SendMessages(bool monitorSingal)
{            
  int counter=1;
  while (monitorSingal)
  {
     if (((ManualResetEvent)Global.GetHandle(Handles.ManualResetEvent)).WaitOne())
     {
        Console.WriteLine("Sending message {0}", counter);
        Thread.Sleep(3000);
        counter += 1;
     }
  }           
} 
 

public static void Demo_ManualResetEvent()
{
  Console.WriteLine("Demo Mnaulreset event...");
  Server s = new Server();
  ThreadPool.QueueUserWorkItem(new WaitCallback((o) =>
  {
    s.SendMessages(true);
  }));
 
  Console.WriteLine("Press 1 to send messages");
  Console.WriteLine("Prress 2 to stop messages");
 
  while (true)
  {               
    int input = Convert.ToInt16(Console.ReadLine());                              
 
    switch (input)
    {
      case 1:
         Console.WriteLine("Starting to send message ...");                        
         ((ManualResetEvent)Global.GetHandle(Handles.ManualResetEvent)).Set();
         break;
      case 2:                                                
         ((ManualResetEvent)Global.GetHandle(Handles.ManualResetEvent)).Reset();
         Console.WriteLine("Message Stopped ..."); 
         break;
      default:
         Console.WriteLine("Invalid Input");
         break;
    }
  }            
} 
 

 

任務Task類

.net 4.0 提出了Task,是ThreadPool的擴展,概念相當美好。我們可以取消任務,等待任務。檢查進度,考慮下面的例子將要用到的三個方法。

static void DoHeavyWork(CancellationToken ct)
{
 try
 {
                while (true)
                {
                    ct.ThrowIfCancellationRequested();
                    Console.WriteLine("Background thread working for task 3..");
                    Thread.Sleep(2000);
                    if (ct.IsCancellationRequested)
                    {
                        ct.ThrowIfCancellationRequested();
                    }
                }
 }
catch (OperationCanceledException ex)
    {
                Console.WriteLine("Exception :" + ex.Message);
       }
 catch (Exception ex)
       {
                Console.WriteLine("Exception :", ex.Message);
        }            
            
}
 

 

static void DoHeavyWork(int n)
{
  Thread.Sleep(5000);
  Console.WriteLine("Operation complete for thread {0}", Thread.CurrentThread.ManagedThreadId);
}
 

static int DoHeavyWorkWithResult(int num)
{
  Thread.Sleep(5000);
  Console.WriteLine("Operation complete for thread {0}", Thread.CurrentThread.ManagedThreadId);
  return num;
}

 

 

我們還有三個task用來運行這三個方法。第一個線程完成沒有返回結果,第二個線程完成並且返回結果,第三個線程在完成之前取消。

try
            {
                Console.WriteLine(DateTime.Now);
                CancellationTokenSource cts1 = new CancellationTokenSource();
                CancellationTokenSource cts2 = new CancellationTokenSource();
                CancellationTokenSource cts3 = new CancellationTokenSource();
 
                Task t1 = new Task((o) => DoHeavyWork(2), cts1.Token);
 
                Console.WriteLine("Starting Task 1");
                Console.WriteLine("Thread1 state {0}", t1.Status);
                t1.Start();
 
                Console.WriteLine("Starting Task 2");
                Task<int> t2 = Task<int>.Factory.StartNew((o) => DoHeavyWorkWithResult(2), cts2.Token);
 
                Console.WriteLine("Starting Task 3");
                Task t3 = new Task((o) => DoHeavyWork(cts3.Token), cts3);
                t3.Start();               
 
                Console.WriteLine("Thread1 state {0}", t1.Status);
                Console.WriteLine("Thread2 state {0}", t2.Status);
                Console.WriteLine("Thread3 state {0}", t3.Status);
                   
                // wait for task 1 to be over
                t1.Wait();
 
                Console.WriteLine("Task 1 complete");
 
                Console.WriteLine("Thread1 state {0}", t1.Status);
                Console.WriteLine("Thread2 state {0}", t2.Status);
                Console.WriteLine("Thread3 state {0}", t3.Status);
 
                //cancel task 3
                Console.WriteLine("Task 3 is : {0} and cancelling...", t3.Status);
                cts3.Cancel();
 
                // wait for task 2 to be over
                t2.Wait();
 
                Console.WriteLine("Task 2 complete");
 
                Console.WriteLine("Thread1 state {0}", t1.Status);
                Console.WriteLine("Thread2 state {0}", t2.Status);
                Console.WriteLine("Thread3 state {0}", t3.Status);
 
                Console.WriteLine("Result {0}", t2.Result);
                Console.WriteLine(DateTime.Now);
 
                t3.Wait();
 
                Console.WriteLine("Task 3 complete");
                Console.WriteLine(DateTime.Now);
            }
            
            catch (Exception ex)
            {
                Console.WriteLine("Exception : " + ex.Message.ToString());
            }
            finally
            {
                Console.Read();
            }

 

 

.net 4.0中並行Parallel編程(時間片)

.net 4.0提出了一個並行編程的很不錯的特性,我們前面所說的大部分線程的例子都是把大量的工作交給空閑線程去做。計算機仍然一次處理一個線程。簡而言之就是,不是真正的多任務執行,而通過Parallel類這就是可能的。

考慮一個Employee類,這個類有一個繁重的操作:ProcessEmployeeInformation

class Employee
{
  public Employee(){}
 
  public int EmployeeID {get;set;}
 
  public void ProcessEmployeeInformation()
  {
    Thread.Sleep(5000);
    Console.WriteLine("Processed Information for Employee {0}",EmployeeID);
  }
} 

 

 

我們創建8個對象,來模擬並行請求,在一個4核的處理器上,4個請求將會同時進行,其余的則會等待。

List<employee> empList = new List<employee>()
 {
   new Employee(){EmployeeID=1},
   new Employee(){EmployeeID=2},
   new Employee(){EmployeeID=3},
   new Employee(){EmployeeID=4},
   new Employee(){EmployeeID=5},
   new Employee(){EmployeeID=6},
   new Employee(){EmployeeID=7},
   new Employee(){EmployeeID=8},
 };
 
 Console.WriteLine("Start Operation {0}", DateTime.Now);
 System.Threading.Tasks.Parallel.ForEach(empList, (e) =>e.ProcessEmployeeInformation());

 

我們可以通過設置MaxDegreeOfParallelism  的值來控制/限制並行任務的數量。如果被設置為-1,就是說沒有限制。。

System.Threading.Tasks.Parallel.For(0, 8, new ParallelOptions() { MaxDegreeOfParallelism = 4 }, (o) =>
       {
          Thread.Sleep(5000);
          Console.WriteLine("Thread ID - {0}", Thread.CurrentThread.ManagedThreadId);
        });

 

 

並行的問題是如果我們開啟了一系列請求,我們不能保持響應也是一樣的順序,順序是不確定的。而AsOrdered屬性可以幫助我們,輸入可以是任何順序,輸出就是對應的順序。

Console.WriteLine("Start Operation {0}", DateTime.Now);
var q = from e in empList.AsParallel().AsOrdered()
        select new { ID = e.EmployeeID };
 
foreach (var item in q)
{
  Console.WriteLine(item.ID);
}
Console.WriteLine("End Operation {0}", DateTime.Now);

 

 

線程安全

關於線程常常討論的一個就是線程安全了。考慮一個被多個線程使用的資源,資源將會以一種不確定的方式被使用,導致結果亂七八糟,這就是我們為什么要實現線程安全的程序,是為了讓資源一次只能被一個線程操作,下面是.net中實現線程安全的一些方法。

Interlocked  這個Interlocked類把操作看作是原子的。比如,簡單的加減法在處理器內部是分為三步的。當多個線程訪問同樣的對象進行這些操作的時候,導致結果混亂,一個建成在執行了前兩步后,被掛起。另一個線程執行了完整的三步,之后,當第一個線程恢復執行的時候他就覆寫了這個值,第二個線程所做的操作就丟失了。因此我們需要看這些操作看作是原子的。使他們能夠線程安全的。比如加減,讀,交換等等。

System.Threading.Interlocked.Increment(object);

 

 

Monitor  這個Monitor類用來鎖住那些有可能多線程下有風險的對象。

if (Monitor.TryEnter(this, 300)) {
    try {
        // code protected by the Monitor here.
    }
    finally {
        Monitor.Exit(this);
    }
}
else {
    // Code if the attempt times out.
}

 

Locks  這個Locks類是Monitor的加強版,最好的一個例子就就是單例類的GetInstance() 方法,多個線程可以訪問這段代碼,因此使用一個syncLock對象鎖住,這個對象和真實世界的鎖很想,如果兩個或多個資源都有要是,他們可以打開鎖並且訪問資源。因此,我們必須確保要是是唯一不共享的。這里就是這個syncLock對象。把這個對象作為私有的變量是很好的。

static object syncLock = new object();
 
if (_instance == null)
{
    lock (syncLock)
    {
        if (_instance == null)
        {
            _instance = new LoadBalancer();
        }
    }
} 

 

Reader-Writer Lock  這個鎖可以被無限制數量的同時讀者請求,或者被一個單一的寫者請求,如果大多數是讀請求很少/時間很短,那么 比Monitor性能更好。讀寫者在不同的隊列里,當寫者擁有鎖的時候,讀者排隊等待寫者完成,當讀者有鎖的時候,所有的寫者排隊。讀者和寫者交替着完成工作,下面的代碼詳細解釋了。有兩個方法。ReadFromCollection  和WriteToCollection 從一個集合里各自的讀/寫。注意AcquireReaderLock  和 AcquireWriterLock 的使用

static void Main(string[] args)
        {
            // Thread 1 writing
            new Thread(new ThreadStart(() =>
                {
                    WriteToCollection(new int[]{1,2,3});
                    
                })).Start();
 
            // Thread 2 Reading
            new Thread(new ThreadStart(() =>
            {
                ReadFromCollection();                
            })).Start();
 
            // Thread 3 Writing
            new Thread(new ThreadStart(() =>
            {
                WriteToCollection(new int[] { 4, 5, 6 });
 
            })).Start();
 
            // Thread 4 Reading
            new Thread(new ThreadStart(() =>
            {
                ReadFromCollection();
            })).Start();            
 
            Console.ReadLine();
        }
 
        static void ReadFromCollection()
        {
            rwLock.AcquireReaderLock(5000);
            try 
            {
                Console.WriteLine("Read Lock acquired by thread : {0}  @ {1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString("hh:mm:ss"));
                Console.Write("Collection : ");
                foreach (int item in myCollection)
                {
                    Console.Write(item + ", ");
                }
                Console.Write("\n");
            }
            catch (Exception ex)
            {
                Console.WriteLine("Exception : " + ex.Message);
            }
            finally
            {
                Console.WriteLine("Read Lock released by thread : {0}  @ {1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString("hh:mm:ss"));
                rwLock.ReleaseReaderLock();
                
            }
        }
 
        static void WriteToCollection(int[] num)
        {
            rwLock.AcquireWriterLock(5000);
            try
            {
                Console.WriteLine("Write Lock acquired by thread : {0}  @ {1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString("hh:mm:ss"));
                myCollection.AddRange(num);
                Console.WriteLine("Written to collection ............: {0}", DateTime.Now.ToString("hh:mm:ss"));
            }
            catch (Exception ex)
            {
                Console.WriteLine("Exception : " + ex.Message);
            }
            finally
            {
                Console.WriteLine("Write Lock released by thread : {0}  @ {1}", Thread.CurrentThread.ManagedThreadId, DateTime.Now.ToString("hh:mm:ss"));
                rwLock.ReleaseWriterLock();                
            }
        }   
 

 

Mutex   Mutex通常用來在操作系統中共享資源,最好的例子就是檢測是否同時有兩個同樣的進程在運行。

 

死鎖

當談論線程安全的時候,死鎖是無法逃避的。

死鎖是兩個/多個線程鎖住了同樣的資源。都在等待對方釋放。會導致操作無休止的等待。死鎖可以通過認真的編程避免。比如:

線程A鎖住對象A

線程A鎖住對象B

線程B鎖住對象B

線程B鎖住對象A

線程A等待線程B釋放對象B,而線程B等待線程A釋放對象A,考慮下面的例子。在一個死鎖類里,我們有兩個方法OperationA和OperationB嵌套的鎖住兩個對象,同時運行兩個方法。會導致死鎖。

public class DeadLock

{       

 static object lockA = new object();

 static object lockB = new object();

 

 public void OperationA()

 {           

  lock (lockA)

  {

   Console.WriteLine("Thread {0} has locked Obect A", Thread.CurrentThread.ManagedThreadId);

   lock (lockB)

   {

    Console.WriteLine("Thread {0} has locked Obect B", Thread.CurrentThread.ManagedThreadId);

   }

   Console.WriteLine("Thread {0} has released Obect B", Thread.CurrentThread.ManagedThreadId);

  }

  Console.WriteLine("Thread {0} has released Obect A", Thread.CurrentThread.ManagedThreadId);

 }

 

 public void OperationB()

 {           

  lock (lockB)

  {

   Console.WriteLine("Thread {0} has locked Obect B", Thread.CurrentThread.ManagedThreadId);

   lock (lockA)

   {

    Console.WriteLine("Thread {0} has locked Obect A", Thread.CurrentThread.ManagedThreadId);

   }

   Console.WriteLine("Thread {0} has released Obect A", Thread.CurrentThread.ManagedThreadId);

  }

  Console.WriteLine("Thread {0} has released Obect B", Thread.CurrentThread.ManagedThreadId);

}

} 

 

DeadLock deadLock = new DeadLock();
 
 Thread tA = new Thread(new ThreadStart(deadLock.OperationA));
 Thread tB = new Thread(new ThreadStart(deadLock.OperationB));
 
 Console.WriteLine("Starting Thread A");
 tA.Start();
                
 Console.WriteLine("Starting Thread B");
 tB.Start();

 

 

工作線程VS I/O線程

操作系統只有一個線程概念,但是.net 對我們抽象出了一層,我們可以處理兩個線程-工作線程和I/O線程,ThreadPool.GetAvailableThreads(out workerThread, out ioThread) 這個方法可以返回給我們可用的每種線程的可用數目。當寫代碼的時候,程序中繁重的任務應該被分為兩部分,計算密集型和I/O密集型。計算密集型是那些CPU運轉較多,比如運行查詢或是復雜的算法的部分。I/O密集型是那些被用來做一些系統I/O硬件或是網絡設備的部分。比如-讀寫文件,從數據庫取數據,查詢遠程web服務器等。計算密集型應該委托給工作線程,I/O密集型應該被委托給I/O線程。如果我們委托工作線程去做I/O密集型的操作,當設備做這個操作的時候,線程會阻塞,阻塞的線程就是浪費的資源,另一方面,如果我們使用I/O線程做同樣的任務,調用線程將會委托任務給設備驅動,自己則回到線程池,當操作完成后,調用線程會從線程池中被通知來處理任務完成。有點事線程保持未阻塞來處理其他的任務,因為當調用線程發起了I/O操作以后,就委托給了操作系統的部分。來處理設備驅動。因此就沒有理由阻塞線程了。在.net的類庫里。有專門的類型處理I/O線程。比如FileStream類里的BeginRead()和EndRead()方法。所有類似的

 

總結

能力越大,責任越大 –線程池

1 .沒有程序應該在UI線程里做繁重的任務。沒有比無響應的UI更難以接受的了。一般情況下,通過使用線程池來管理線程異步執行一些繁重的任務。

2 .UI不能直接在非UI或是后台線程里更新。程序要需要委托這類工作給UI線程。這可以通過在winform里使用Invoke方法。在WPF里使用Dispatcher方法。或是使用BackGroundWorker自動處理。

3 .線程是很昂貴的資源應該被認真對待。“越多越熱鬧”顯然是不能接受的。

4 .在我們的程序里的問題不會通過簡單的把工作交給另一個線程就能解決。不會有神奇的事情發生。程序需要被合理的設計。來獲得高效率。

5 .通過Thread類來創建縣城的時候要萬分小心。調整線程優先級應該慎重。又可以導致其他重要的線程不能執行。

6 .胡亂設置IsBackground為false可能會引發無法預料的錯誤。前台線程直到完成才會讓程序終止,如果用戶想要終止將程序,結果運行在后台的一個任務被設置為了前台線程。導致程序無法終止了。

7 .多程序中多線程貢獻資源的時候,異步線程技術要小心,死鎖可以通過認真的編碼避免一部分。

8 .程序員應該確保線程不要太多。閑置線程可能增加開銷並且導致內存溢出。

9 .I/O操作必須委托給I/O線程而不是工作線程。

譯自:http://www.codeproject.com/Articles/212377/Multithreading-Demystified

我的博客:http://leaver.me

如果有任何問題,還望大家手下留情。歡迎指出。歡迎交流。


免責聲明!

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



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