熱點推薦:
您现在的位置: 電腦知識網 >> 編程 >> .NET編程 >> 正文

C#中構建多線程應用程序

2022-06-13   來源: .NET編程 

  引言

  隨著雙核四核等多核處理器的推廣多核處理器或超線程單核處理器的計算機已很常見基於多核處理的編程技術也開始受到程序員們普遍關注這其中一個重要的方面就是構建多線程應用程序(因為不使用多線程的話開發人員就不能充分發揮多核計算機的強大性能)

  本文針對的是構建基於單核計算機的多線程應用程序目的在於介紹多線程相關的基本概念內涵以及如何通過SystemThreading命名空間的類委托和BackgroundWorker組件等三種手段構建多線程應用程序

  本文如果能為剛接觸多線程的朋友起到拋磚引玉的作用也就心滿意足了當然本人才疏學淺文中難免會有不足或錯誤的地方懇請各位朋友多多指點

  理解多線程

  我們通常理解的應用程序就是一個*exe文件當運行*exe應用程序以後系統會在內存中為該程序分配一定的空間同時加載一些該程序所需的資源其實這就可以稱為創建了一個進程可以通過Windows任務管理器查看這個進程的相關信息如映像名稱用戶名內存使用PID(唯一的進程標示)等如圖下所示

  

  而線程則只是進程中的一個基本執行單元一個應用程序往往只有一個程序入口

  [STAThread]

  static void Main()   //應用程序主入口點

  {

  ApplicationEnableVisualStyles();

  ApplicationSetCompatibleTextRenderingDefault(false);

  ApplicationRun(new MainForm());

  }

  進程會包含一個進入此入口的線程我們稱之為主線程其中特性 [STAThread] 指示應用程序的默認線程模型是單線程單元(相關信息可參考us/library/systemstathreadattribute(VS)aspx)只包含一個主線程的進程是線程安全的相當於程序僅有一條工作線只有完成了前面的任務才能執行排在後面的任務

  然當在程序處理一個很耗時的任務如輸出一個大的文件或遠程訪問數據庫等此時的窗體界面程序對用戶而言基本像是沒反應一樣菜單按鈕等都用不了因為窗體上控件的響應事件也是需要主線程來執行的而主線程正忙著干其他的事控件響應事件就只能排隊等著主線程忙完了再執行

  為了克服單線程的這個缺陷Win API可以讓主線程再創建其他的次線程但不論是主線程還是次線程都是進程中獨立的執行單元可以同時訪問共享的數據這樣就有了多線程這個概念

  相信到這應該對多線程有個比較感性的認識了但筆者在這要提醒一下基於單核計算機的多線程其實只是操作系統施展的一個障眼法而已(但這不會干擾我們理解構建多線程應用程序的思路)他並不能縮短完成所有任務的時間有時反而還會因為使用過多的線程而降低性能延長時間之所以這樣是因為對於單CPU而言在一個單位時間(也稱時間片)內只能執行一個線程即只能干一件事當一個線程的時間片用完時系統會將該線程掛起下一個時間內再執行另一個線程如此CPU以時間片為間隔在多個線程之間交替執行運算(其實這裡還與每個線程的優先級有關級別高的會優先處理)由於交替時間間隔很短所以造成了各個線程都在同時工作的假象而如果線程數目過多由於系統掛起線程時要記錄線程當前的狀態數據等這樣又勢必會降低程序的整體性能但對於這些多核計算機就能從本質上(真正的同時工作)提高程序的執行效率

   線程異步與線程同步

  從線程執行任務的方式上可以分為線程同步和線程異步而為了方便理解後面描述中用同步線程指代與線程同步相關的線程同樣異步線程表示與線程異步相關的線程

  線程異步就是解決類似前面提到的執行耗時任務時界面控件不能使用的問題如創建一個次線程去專門執行耗時的任務而其他如界面控件響應這樣的任務交給另一個線程執行(往往由主線程執行)這樣兩個線程之間通過線程調度器短時間(時間片)內的切換就模擬出多個任務同時被執行的效果

  線程異步往往是通過創建多個線程執行多個任務多個工作線同時開工類似多輛在寬廣的公路上並行的汽車同時前進互不干擾(讀者要明白本質上並沒有同時僅僅是操作系統玩的一個障眼法但這個障眼法卻對提高我們的程序與用戶之間的交互以及提高程序的友好性很有用不是嗎)

  在介紹線程同步之前先介紹一個與此緊密相關的概念——並發問題

  前面提到線程都是獨立的執行單元可以訪問共享的數據也就是說在一個擁有多個次線程的程序中每個線程都可以訪問同一個共享的數據再稍加思考你會發現這樣可能會出問題由於線程調度器會隨機的掛起某一個線程(前面介紹的線程間的切換)所以當線程a對共享數據D的訪問(修改刪除等操作)完成之前被掛起而此時線程b又恰好去訪問數據D那麼線程b訪問的則是一個不穩定的數據這樣就會產生非常難以發現bug由於是隨機發生的產生的結果是不可預測的這樣樣的bug也都很難重現和調試這就是並發問題

  為了解決多線程共同訪問一個共享資源(也稱互斥訪問)時產生的並發問題線程同步就應運而生了線程同步的機理簡單的說就是防止多個線程同時訪問某個共享的資源做法很簡單標記訪問某共享資源的那部分代碼當程序運行到有標記的地方時CLR(具體是什麼可以先不管只要知道它能控制就行)對各線程進行調整如果已有線程在訪問一資源CLR就會將其他訪問這一資源的線程掛起直到前一線程結束對該資源的訪問這樣就保證了同一時間只有一個線程訪問該資源打個比方就如某資源放在只有一獨木橋相連的孤島上如果要使用該資源大家就得排隊一個一個來前面的回來了下一個再去前面的沒回來後面的就原地待命

  這裡只是把基本的概念及原理做了一個簡單的闡述不至於看後面的程序時糊裡糊塗的具體如何編寫代碼下面的段落將做詳細介紹

  創建多線程應用程序

  這裡做一個簡單的說明下面主要通過介紹通過SystemThreading命名空間的類委托和BackgroundWorker組件三種不同的手段構建多線程應用程序具體會從線程異步和線程同步兩個方面來闡述

  通過SystemThreading命名空間的類構建

  在NET平台下SystemThreading命名空間提供了許多類型來構建多線程應用程序可以說是專為多線程服務的由於本文僅是想起到一個拋磚引玉的作用所以對於這一塊不會探討過多過深主要使用SystemThreadingThread類

  先從SystemThreadingThread類本身相關的一個小例子說起代碼如下解釋見注釋

  using System;

  using SystemThreading; //引入SystemThreading命名空間

  namespace MultiThread

  {

  class Class

  {

  static void Main(string[] args)

  {

  ConsoleWriteLine(************** 顯示當前線程的相關信息 *************);

  //聲明線程變量並賦值為當前線程

  Thread primaryThread = ThreadCurrentThread;

  //賦值線程的名稱

  primaryThreadName = 主線程;

  //顯示線程的相關信息

  ConsoleWriteLine(線程的名字{} primaryThreadName);

  ConsoleWriteLine(線程是否啟動? {} primaryThreadIsAlive);

  ConsoleWriteLine(線程的優先級 {} primaryThreadPriority);

  ConsoleWriteLine(線程的狀態 {} primaryThreadThreadState);

  ConsoleReadLine();

  }

  }

  }

  輸出結果如下

  ************** 顯示當前線程的相關信息 *************

  線程的名字主線程

  線程是否啟動? True

  線程的優先級 Normal

  線程的狀態 Running

  對於上面的代碼不想做過多解釋只說一下ThreadCurrentThread得到的是執行當前代碼的線程

  異步調用線程

  這裡先說一下前台線程與後台線程前台線程能阻止應用程序的終止既直到所有前台線程終止後才會徹底關閉應用程序而對後台線程而言當所有前台線程終止時後台線程會被自動終止不論後台線程是否正在執行任務默認情況下通過ThreadStart()方法創建的線程都自動為前台線程把線程的屬性IsBackground設為true時就將線程轉為後台線程

  下面先看一個例子該例子創建一個次線程執行打印數字的任務而主線程則干其他的事兩者同時進行互不干擾

  using System;

  using SystemThreading;

  using SystemWindowsForms;

  namespace MultiThread

  {

  class Class

  {

  static void Main(string[] args)

  {

  ConsoleWriteLine(************* 兩個線程同時工作 *****************);

  //主線程因為獲得的是當前在執行Main()的線程

  Thread primaryThread = ThreadCurrentThread;

  primaryThreadName = 主線程;

  ConsoleWriteLine(> {} 在執行主函數 Main() ThreadCurrentThreadName);

  //次線程該線程指向PrintNumbers()方法

  Thread SecondThread = new Thread(new ThreadStart(PrintNumbers));

  SecondThreadName = 次線程;

  //次線程開始執行指向的方法

  SecondThreadStart();

  //同時主線程在執行主函數中的其他任務

  MessageBoxShow(正在執行主函數中的任務 主線程在工作);

  ConsoleReadLine();

  }

  //打印數字的方法

  static void PrintNumbers()

  {

  ConsoleWriteLine(> {} 在執行打印數字函數 PrintNumber() ThreadCurrentThreadName);

  ConsoleWriteLine(打印數字 );

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

  {

  ConsoleWrite({} i);

  //Sleep()方法使當前線程掛等待指定的時長在執行這裡主要是模仿打印任務

  ThreadSleep();

  }

  ConsoleWriteLine();

  }

  }

  }

  程序運行後會看到一個窗口彈出如圖所示同時控制台窗口也在不斷的顯示數字

  

  輸出結果為

  ************* 兩個線程同時工作 *****************

  > 主線程 在執行主函數 Main()

  > 次線程 在執行打印數字函數 PrintNumber()

  打印數字

  

  這裡稍微對 Thread SecondThread = new Thread(new ThreadStart(PrintNumbers)); 這一句做個解釋其實 ThreadStart 是 SystemThreading 命名空間下的一個委托其聲明是 public delegate void ThreadStart()指向不帶參數返回值為空的方法所以當使用 ThreadStart 時對應的線程就只能調用不帶參數返回值為空的方法那非要指向含參數的方法呢?在SystemThreading命名空間下還有一個ParameterizedThreadStart 委托其聲明是 public delegate void ParameterizedThreadStart(object obj)可以指向含 object 類型參數的方法這裡不要忘了 object 可是所有類型的父類哦有了它就可以通過創建各種自定義類型如結構類等傳遞很多參數了這裡就不再舉例說明了

  並發問題

  這裡再通過一個例子讓大家切實體會一下前面說到的並發問題然後再介紹線程同步

  using System;

  using SystemThreading;

  namespace MultiThread

  {

  class Class

  {

  static void Main(string[] args)

  {

  ConsoleWriteLine(********* 並發問題演示 ***************);

  //創建一個打印對象實例

  Printer printer = new Printer();

  //聲明一含個線程對象的數組

  Thread[] threads = new Thread[];

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

  {

  //將每一個線程都指向printer的PrintNumbers()方法

  threads[i] = new Thread(new ThreadStart(printerPrintNumbers));

  //給每一個線程編號

  threads[i]Name = iToString() +號線程;

  }

  //開始執行所有線程

  foreach (Thread t in threads)

  tStart();

  ConsoleReadLine();

  }

  }

  //打印類

  public class Printer

  {

  //打印數字的方法

  public void PrintNumbers()

  {

  ConsoleWriteLine(> {} 正在執行打印任務開始打印數字 ThreadCurrentThreadName);

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

  {

  Random r = new Random();

  //為了增加沖突的幾率及使各線程各自等待隨機的時長

  ThreadSleep( * rNext());

  //打印數字

  ConsoleWrite({} i);

  }

  ConsoleWriteLine();

  }

  }

  }

  上面的例子中主線程產生的個線程同時訪問同一個對象實例printer的方法PrintNumbers()由於沒有鎖定共享資源(注意這裡是指控制台)所以在PrintNumbers()輸出到控制台之前調用PrintNumbers()的線程很可能被掛起但不知道什麼時候(或是否有)掛起導致得到不可預測的結果如下是兩個不同的結果(當然讀者的運行結果可能會是其他情形)

  

  情形一

  

  情形二

  線程同步

  線程同步的訪問方式也稱為阻塞調用即沒有執行完任務不返回線程被掛起可以使用C#中的lock關鍵字在此關鍵字范圍類的代碼都將是線程安全的lock關鍵字需定義一個標記線程進入鎖定范圍是必須獲得這個標記當鎖定的是一個實例級對象的私有方法時使用方法本身所在對象的引用就可以了將上面例子中的打印類Printer稍做改動添加lock關鍵字代碼如下

  //打印類

  public class Printer

  {

  public void PrintNumbers()

  {

  //使用lock關鍵字鎖定d的代碼是線程安全的

  lock (this)

  {

  ConsoleWriteLine(> {} 正在執行打印任務開始打印數字 ThreadCurrentThreadName);

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

  {

  Random r = new Random();

  //為了增加沖突的幾率及使各線程各自等待隨機的時長

  ThreadSleep( * rNext());

  //打印數字

  ConsoleWrite({} i);

  }

  ConsoleWriteLine();

  }

  }

  }

  }

  同步後執行結果如下

  

  也可以使用SystemThreading命名空間下的Monitor類進行同步兩者內涵是一樣的但Monitor類更靈活這裡就不在做過多的探討代碼如下

  //打印類

  public class Printer

  {

  public void PrintNumbers()

  {

  MonitorEnter(this);

  try

  {

  ConsoleWriteLine(> {} 正在執行打印任務開始打印數字 ThreadCurrentThreadName);

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

  {

  Random r = new Random();

  //為了增加沖突的幾率及使各線程各自等待隨機的時長

  ThreadSleep( * rNext());

  //打印數字

  ConsoleWrite({} i);

  }

  ConsoleWriteLine();

  }

  finally

  {

  MonitorExit(this);

  }

  }

  }

  輸出結果與上面的一樣

  通過委托構建多線程應用程序

  在看下面的內容時要求對委托有一定的了解如果不清楚的話推薦參考一下博客園張子陽的《C# 中的委托和事件》裡面對委托與事件進行由淺入深的較系統的講解

  這裡先舉一個關於委托的簡單例子具體解說見注釋

  using System;

  namespace MultiThread

  {

  //定義一個指向包含兩個int型參數返回值為int型的函數的委托

  public delegate int AddOp(int x int y);

  class Program

  {

  static void Main(string[] args)

  {

  //創建一個指向Add()方法的AddOp對象p

  AddOp pAddOp = new AddOp(Add);

  //使用委托間接調用方法Add()

  ConsoleWriteLine( + = {} pAddOp( ));

  ConsoleReadLine();

  }

  //求和的函數

  static int Add(int x int y)

  {

  int sum = x + y;

  return sum;

  }

  }

  }

  運行結果為

   + =

  線程異步

  先說明一下這裡不打算講解委托線程異步或同步的參數傳遞獲取返回值等只是做個一般性的開頭而已如果後面有時間了再另外寫一篇關於多線程中參數傳遞獲取返回值的文章

  注意觀察上面的例子會發現直接使用委托實例 pAddOp( ) 就調用了求和方法 Add()很明顯這個方法是由主線程執行的然而委托類型中還有另外兩個方法——BeginInvoke()和EndInvoke()下面通過具體的例子來說明將上面的例子做適當改動如下

  using System;

  using SystemThreading;

  using SystemRuntimeRemotingMessaging;

  namespace MultiThread

  {

  //聲明指向含兩個int型參數返回值為int型的函數的委托

  public delegate int AddOp(int x int y);

  class Program

  {

  static void Main(string[] args)

  {

  ConsoleWriteLine(******* 委托異步線程 兩個線程同時工作 *********);

  //顯示主線程的唯一標示

  ConsoleWriteLine(調用Main()的主線程的線程ID是{} ThreadCurrentThreadManagedThreadId);

  //將委托實例指向Add()方法

  AddOp pAddOp = new AddOp(Add);

  //開始委托次線程調用委托BeginInvoke()方法返回的類型是IAsyncResult

  //包含這委托指向方法結束返回的值同時也是EndInvoke()方法參數

  IAsyncResult iftAR = pAddOpBeginInvoke( null null);

  ConsoleWriteLine(nMain()方法中執行其他任務n);

  int sum = pAddOpEndInvoke(iftAR);

  ConsoleWriteLine( + = {} sum);

  ConsoleReadLine();

  }

  //求和方法

  static int Add(int x int y)

  {

  //指示調用該方法的線程IDManagedThreadId是線程的唯一標示

  ConsoleWriteLine(調用求和方法 Add()的線程ID是 {} ThreadCurrentThreadManagedThreadId);

  //模擬一個過程停留

  ThreadSleep();

  int sum = x + y;

  return sum;

  }

  }

  }

  運行結果如下

  ******* 委托異步線程 兩個線程同時工作 *********

  調用Main()的主線程的線程ID是

  Main()方法中執行其他任務

  調用求和方法 Add()的線程ID是

   + =

  線程同步

  委托中的線程同步主要涉及到上面使用的pAddOpBeginInvoke( null null)方法中後面兩個為null的參數具體的可以參考相關資料這裡代碼如下解釋見代碼注釋

  using System;

  using SystemThreading;

  using SystemRuntimeRemotingMessaging;

  namespace MultiThread

  {

  //聲明指向含兩個int型參數返回值為int型的函數的委托

  public delegate int AddOp(int x int y);

  class Program

  {

  static void Main(string[] args)

  {

  ConsoleWriteLine(******* 線程同步阻塞調用兩個線程工作 *********);

  ConsoleWriteLine(Main() invokee on thread {} ThreadCurrentThreadManagedThreadId);

  //將委托實例指向Add()方法

  AddOp pAddOp = new AddOp(Add);

  IAsyncResult iftAR = pAddOpBeginInvoke( null null);

  //判斷委托線程是否執行完任務

  //沒有完成的話主線程就做其他的事

  while (!iftARIsCompleted)

  {

  ConsoleWriteLine(Main()方法工作中);

  ThreadSleep();

  }

  //獲得返回值

  int answer = pAddOpEndInvoke(iftAR);

  ConsoleWriteLine( + = {} answer);

  ConsoleReadLine();

  }

  //求和方法

  static int Add(int x int y)

  {

  //指示調用該方法的線程IDManagedThreadId是線程的唯一標示

  ConsoleWriteLine(調用求和方法 Add()的線程ID是 {} ThreadCurrentThreadManagedThreadId);

  //模擬一個過程停留

  ThreadSleep();

  int sum = x + y;

  return sum;

  }

  }

  }

  運行結果如下

  ******* 線程同步阻塞調用兩個線程工作 *********

  Main() invokee on thread

  Main()方法工作中

  調用求和方法 Add()的線程ID是

  Main()方法工作中

  Main()方法工作中

  Main()方法工作中

  Main()方法工作中

   + =

  BackgroundWorker組件

  BackgroundWorker組件位於工具箱中用於方便的創建線程異步的程序新建一個WindowsForms應用程序界面如下

  

  代碼如下解釋參見注釋

  private void button_Click(object sender EventArgs e)

  {

  try

  {

  //獲得輸入的數字

  int numOne = intParse(thistextBoxText);

  int numTwo = intParse(thistextBoxText);

  //實例化參數類

  AddParams args = new AddParams(numOne numTwo);

  //調用RunWorkerAsync()生成後台線程同時傳入參數

  thisbackgroundWorkerRunWorkerAsync(args);

  }

  catch (Exception ex)

  {

  MessageBoxShow(exMessage);

  }

  }

  //backgroundWorker新生成的線程開始工作

  private void backgroundWorker_DoWork(object sender DoWorkEventArgs e)

  {

  //獲取傳入的AddParams對象

  AddParams args = (AddParams)eArgument;

  //停留模擬耗時任務

  ThreadSleep();

  //返回值

  eResult = argsa + argsb;

  }

  //當backgroundWorker的DoWork中的代碼執行完後會觸發該事件

  //同時其執行的結果會包含在RunWorkerCompletedEventArgs參數中

  private void backgroundWorker_RunWorkerCompleted(object sender RunWorkerCompletedEventArgs e)

  {

  //顯示運算結果

  MessageBoxShow(運行結果為 + eResultToString() 結果);

  }

  }

  //參數類這個類僅僅起到一個記錄並傳遞參數的作用

  class AddParams

  {

  public int a b;

  public AddParams(int numb int numb)

  {

  a = numb;

  b = numb;

  }

  }

  注意在計算結果的同時窗體可以隨意移動也可以重新在文本框中輸入信息這就說明主線程與backgroundWorker組件生成的線程是異步的

  總結

  本文從線程進程應用程序的關系開始介紹了一些關於多線程的基本概念同時闡述了線程異步線程同步及並發問題等最後從應用角度出發介紹了如何通過SystemThreading命名空間的類委托和BackgroundWorker組件等三種手段構建多線程應用程序


From:http://tw.wingwit.com/Article/program/net/201311/11400.html
    推薦文章
    Copyright © 2005-2022 電腦知識網 Computer Knowledge   All rights reserved.